codex-agent-framework 0.1.55__tar.gz → 0.1.56__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/CHANGELOG.md +10 -0
  2. {codex_agent_framework-0.1.55/codex_agent_framework.egg-info → codex_agent_framework-0.1.56}/PKG-INFO +1 -1
  3. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/client.py +34 -4
  4. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/server/app.py +26 -3
  5. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/app.py +72 -45
  6. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/chat.py +4 -1
  7. codex_agent_framework-0.1.56/codex_agent/tui/perf.py +73 -0
  8. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/state.py +34 -2
  9. codex_agent_framework-0.1.56/codex_agent/tui/stream_debug.py +84 -0
  10. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/style.py +49 -0
  11. codex_agent_framework-0.1.56/codex_agent/tui/widgets.py +248 -0
  12. codex_agent_framework-0.1.56/codex_agent/version.py +1 -0
  13. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56/codex_agent_framework.egg-info}/PKG-INFO +1 -1
  14. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent_framework.egg-info/SOURCES.txt +3 -0
  15. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/pyproject.toml +1 -1
  16. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_chat.py +47 -58
  17. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_server.py +50 -0
  18. codex_agent_framework-0.1.55/codex_agent/version.py +0 -1
  19. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/LICENSE +0 -0
  20. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/MANIFEST.in +0 -0
  21. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/README.md +0 -0
  22. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/__init__.py +0 -0
  23. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/__main__.py +0 -0
  24. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/agent.py +0 -0
  25. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/agent_runtime.py +0 -0
  26. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/ai.py +0 -0
  27. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/browser.py +0 -0
  28. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_commands.py +0 -0
  29. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/__init__.py +0 -0
  30. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  31. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  32. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  33. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  34. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  35. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  36. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/content/system.py +0 -0
  37. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/content/vision.py +0 -0
  38. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/context/__init__.py +0 -0
  39. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  40. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  41. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  42. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  43. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  44. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  45. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/files/tools.py +0 -0
  46. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  47. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  48. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  49. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  50. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  51. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  52. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  53. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  54. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  55. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  56. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  57. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  58. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  59. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  60. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  61. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  62. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  63. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  64. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  65. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  66. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  67. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  68. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  69. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  70. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/builtin_plugins/web_search/__init__.py +0 -0
  71. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/cli/__init__.py +0 -0
  72. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/cli/headless.py +0 -0
  73. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/cli/main.py +0 -0
  74. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/cli/output.py +0 -0
  75. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/cli/root.py +0 -0
  76. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/cli/runner.py +0 -0
  77. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/command.py +0 -0
  78. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/config.py +0 -0
  79. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/context.py +0 -0
  80. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/desktop.py +0 -0
  81. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/event.py +0 -0
  82. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/get_text/__init__.py +0 -0
  83. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/get_text/default_gitignore +0 -0
  84. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/get_text/get_text.py +0 -0
  85. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/get_text/simpler_get_text.py +0 -0
  86. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/hooks.py +0 -0
  87. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/image.py +0 -0
  88. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/latex.py +0 -0
  89. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/mainloop.py +0 -0
  90. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/message.py +0 -0
  91. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/plugin.py +0 -0
  92. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/prompts/system_prompt.txt +0 -0
  93. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/provider.py +0 -0
  94. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/runtime.py +0 -0
  95. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/scheduler.py +0 -0
  96. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  97. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/server/__init__.py +0 -0
  98. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/server/core.py +0 -0
  99. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/service.py +0 -0
  100. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/sessions.py +0 -0
  101. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/status.py +0 -0
  102. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/stream.py +0 -0
  103. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/stream_utils.py +0 -0
  104. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tool.py +0 -0
  105. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tray.py +0 -0
  106. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/__init__.py +0 -0
  107. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/lifecycle.py +0 -0
  108. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/log.py +0 -0
  109. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/status_bar.py +0 -0
  110. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/tui/transcript.py +0 -0
  111. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/utils.py +0 -0
  112. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent/worker.py +0 -0
  113. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  114. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  115. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent_framework.egg-info/requires.txt +0 -0
  116. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/codex_agent_framework.egg-info/top_level.txt +0 -0
  117. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/dependencies.txt +0 -0
  118. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/setup.cfg +0 -0
  119. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent.py +0 -0
  120. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_audio_hooks.py +0 -0
  121. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_commands.py +0 -0
  122. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_context.py +0 -0
  123. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_desktop.py +0 -0
  124. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_environment.py +0 -0
  125. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_image_generation.py +0 -0
  126. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_observe.py +0 -0
  127. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_plugins.py +0 -0
  128. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_providers_config.py +0 -0
  129. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_realtime.py +0 -0
  130. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_response.py +0 -0
  131. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_runtime.py +0 -0
  132. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_sessions.py +0 -0
  133. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_subagents.py +0 -0
  134. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_tools.py +0 -0
  135. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_agent_turns.py +0 -0
  136. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_ai.py +0 -0
  137. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_browser.py +0 -0
  138. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_builtin_config.py +0 -0
  139. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_cli.py +0 -0
  140. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_cli_headless.py +0 -0
  141. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_cli_root.py +0 -0
  142. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_client.py +0 -0
  143. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_events.py +0 -0
  144. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_get_text_browser.py +0 -0
  145. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_hooks.py +0 -0
  146. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_image_message.py +0 -0
  147. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_local_desktop.py +0 -0
  148. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_memory.py +0 -0
  149. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_messages.py +0 -0
  150. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_planner.py +0 -0
  151. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_scheduler.py +0 -0
  152. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_service.py +0 -0
  153. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_tui.py +0 -0
  154. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_utils.py +0 -0
  155. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.56}/tests/test_worker.py +0 -0
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning where practical.
6
6
 
7
+ ## [0.1.56] - 2026-05-17
8
+ ### Changed
9
+ - Replace the TUI chat surface with a widget-based timeline inspired by Textual chat patterns so live assistant output updates an active message widget instead of mutating a monolithic transcript buffer.
10
+ - Add compact TUI event streams that truncate verbose tool/slash result payloads for live UI subscribers while preserving full replay/API data outside compact mode.
11
+ - Add persistent TUI stream diagnostics in `~/.codex-agent/tui-stream-debug.jsonl` and lightweight perf snapshots in `~/.codex-agent/tui-perf.json` to capture intermittent live-stream stalls after the fact.
12
+ - Preserve the previous sparse terminal look with role headers, compact tool/info/error rows, a right-aligned step marker, and sticky-only autoscroll.
13
+
14
+ ### Tests
15
+ - Validate the TUI/server/client changes with targeted chat/client/server coverage and the full offline CI suite (570 passed).
16
+
7
17
  ## [0.1.55] - 2026-05-17
8
18
  ### Fixed
9
19
  - Decouple SSE event-stream consumption from local client/TUI event handling so slow or blocked UI work no longer stalls event intake during long-running or heavy tool activity.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.55
3
+ Version: 0.1.56
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -14,11 +14,12 @@ from .utils import snake_case_type
14
14
  class AgentClient:
15
15
  """Small HTTP/SSE client for a codex-agent server."""
16
16
 
17
- def __init__(self, base_url="http://127.0.0.1:8765", timeout=10, event_read_timeout=15.0, event_stale_after=5.0):
17
+ def __init__(self, base_url="http://127.0.0.1:8765", timeout=10, event_read_timeout=15.0, event_stale_after=5.0, compact_events=False):
18
18
  self.base_url = base_url.rstrip("/")
19
19
  self.timeout = timeout
20
20
  self.event_read_timeout = event_read_timeout
21
21
  self.event_stale_after = event_stale_after
22
+ self.compact_events = compact_events
22
23
  self.client_pid = os.getpid()
23
24
  self._event_handlers = {}
24
25
  self._event_thread = None
@@ -36,6 +37,16 @@ class AgentClient:
36
37
  self.health_interval = 1.0
37
38
  self.health_failure_threshold = 3
38
39
  self._health_failures = 0
40
+ self.stream_debug = None
41
+
42
+ def debug_stream(self, stage, event=None, **fields):
43
+ logger = getattr(self, "stream_debug", None)
44
+ if logger is None:
45
+ return None
46
+ try:
47
+ return logger.record(stage, event, **fields)
48
+ except Exception:
49
+ return None
39
50
 
40
51
  def on(self, event_type, handler=None):
41
52
  if handler is None:
@@ -73,9 +84,13 @@ class AgentClient:
73
84
  seen.add(handler)
74
85
  handlers.append(handler)
75
86
  for handler in handlers:
87
+ handler_name = getattr(handler, "__name__", handler.__class__.__name__)
88
+ self.debug_stream("handler_begin", event, handler=handler_name)
76
89
  try:
77
90
  handler(event)
91
+ self.debug_stream("handler_done", event, handler=handler_name)
78
92
  except Exception as exc:
93
+ self.debug_stream("handler_error", event, handler=handler_name, error=str(exc))
79
94
  if event_type != "client_event_error":
80
95
  self.emit_local({"type": "client_event_error", "error": str(exc), "event": event_type})
81
96
  return event
@@ -89,7 +104,9 @@ class AgentClient:
89
104
 
90
105
  def _enqueue_local(self, event):
91
106
  self._start_dispatch_thread()
92
- self._local_event_queue.put(modict(event))
107
+ event = modict(event)
108
+ self.debug_stream("queue_put", event, queue_size=self._local_event_queue.qsize())
109
+ self._local_event_queue.put(event)
93
110
  return event
94
111
 
95
112
  def _dispatch_loop(self):
@@ -102,7 +119,13 @@ class AgentClient:
102
119
  continue
103
120
  if event is None:
104
121
  break
105
- self.emit_local(event)
122
+ self.debug_stream("dispatch_begin", event, queue_size=self._local_event_queue.qsize())
123
+ try:
124
+ self.emit_local(event)
125
+ self.debug_stream("dispatch_done", event, queue_size=self._local_event_queue.qsize())
126
+ except Exception as exc:
127
+ self.debug_stream("dispatch_error", event, error=str(exc), queue_size=self._local_event_queue.qsize())
128
+ raise
106
129
 
107
130
  def start_events(self):
108
131
  if self._event_thread and self._event_thread.is_alive():
@@ -226,11 +249,14 @@ class AgentClient:
226
249
  self._set_server_connected()
227
250
  stream_ended = True
228
251
  try:
229
- for event in self.iter_sse(response.iter_lines(chunk_size=1, decode_unicode=True)):
252
+ # Keep SSE latency low for interactive streams without
253
+ # falling back to the old byte-by-byte reader.
254
+ for event in self.iter_sse(response.iter_lines(chunk_size=16, decode_unicode=True)):
230
255
  if self._event_stop.is_set():
231
256
  stream_ended = False
232
257
  break
233
258
  self._record_event_stream_activity()
259
+ self.debug_stream("sse_received", event)
234
260
  self.remember_event_sequence(event)
235
261
  self._enqueue_local(event)
236
262
  finally:
@@ -324,6 +350,8 @@ class AgentClient:
324
350
  self._last_event_sequence = sequence
325
351
  def events_url(self):
326
352
  params = {"client_pid": int(self.client_pid)}
353
+ if self.compact_events:
354
+ params["compact"] = 1
327
355
  if self._last_event_sequence is not None:
328
356
  params["after_sequence"] = int(self._last_event_sequence)
329
357
  return self.url(f"/events?{urlencode(params)}")
@@ -437,6 +465,8 @@ class AgentClient:
437
465
  def replay_events(self, before_sequence=None, session_id=None):
438
466
  path = "/events/replay"
439
467
  params = {}
468
+ if self.compact_events:
469
+ params["compact"] = 1
440
470
  if before_sequence is not None:
441
471
  params["before_sequence"] = int(before_sequence)
442
472
  if session_id:
@@ -1,5 +1,7 @@
1
1
  import json
2
2
 
3
+ from modict import modict
4
+
3
5
  from ..agent import Agent
4
6
  from ..version import __version__
5
7
  from ..agent_runtime import InProcessAgentRuntime, ProcessAgentRuntime
@@ -61,6 +63,22 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
61
63
  def jsonable(value):
62
64
  return json.loads(json.dumps(value, ensure_ascii=False, default=str))
63
65
 
66
+ def compact_event_payload(payload, *, max_content_chars=2_000):
67
+ payload = modict(jsonable(payload))
68
+ event_type = payload.get("type")
69
+ if event_type in {"tool_call_done_event", "slash_command_done_event"}:
70
+ content_key = "content" if "content" in payload else "result" if "result" in payload else None
71
+ if content_key is not None:
72
+ content = "" if payload.get(content_key) is None else str(payload.get(content_key))
73
+ if len(content) > max_content_chars:
74
+ payload[f"{content_key}_truncated"] = True
75
+ payload[f"{content_key}_full_length"] = len(content)
76
+ payload[content_key] = content[:max_content_chars] + "\n… truncated for compact event stream …"
77
+ return payload
78
+
79
+ def event_payload(payload, compact=False):
80
+ return compact_event_payload(payload) if compact else payload
81
+
64
82
  def busy_guard(call):
65
83
  try:
66
84
  return call()
@@ -162,7 +180,7 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
162
180
  return jsonable(service.interrupt(request.reason))
163
181
 
164
182
  @app.get("/events")
165
- def events(limit: int | None = None, after_sequence: int | None = None, client_pid: int | None = None):
183
+ def events(limit: int | None = None, after_sequence: int | None = None, client_pid: int | None = None, compact: bool = False):
166
184
  try:
167
185
  subscription = service.subscribe(after_sequence=after_sequence, client_pid=client_pid)
168
186
  except RuntimeError as exc:
@@ -171,6 +189,7 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
171
189
  def stream():
172
190
  count = 0
173
191
  for payload in subscription:
192
+ payload = event_payload(payload, compact=compact)
174
193
  lines = []
175
194
  if payload.get("id"):
176
195
  lines.append(f"id: {payload.id}")
@@ -183,8 +202,12 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
183
202
  return StreamingResponse(stream(), media_type="text/event-stream")
184
203
 
185
204
  @app.get("/events/replay")
186
- def replay_events(before_sequence: int | None = None, session_id: str | None = None):
187
- return jsonable(service.replay_events(before_sequence=before_sequence, session_id=session_id))
205
+ def replay_events(before_sequence: int | None = None, session_id: str | None = None, compact: bool = False):
206
+ replay = service.replay_events(before_sequence=before_sequence, session_id=session_id)
207
+ if compact:
208
+ replay = modict(replay)
209
+ replay.events = [compact_event_payload(event) for event in replay.get("events") or []]
210
+ return jsonable(replay)
188
211
 
189
212
  return app
190
213
 
@@ -6,9 +6,10 @@ from textual.events import Resize
6
6
  from textual.widgets import Footer, Static, TextArea
7
7
 
8
8
  from .lifecycle import clear_tui, install_tui_cleanup_handlers
9
- from .log import SelectableRichLog
9
+ from .perf import TuiPerfMonitor
10
+ from .stream_debug import TuiStreamDebugLog
10
11
  from .style import CHAT_CSS
11
- from .transcript import TranscriptRenderer
12
+ from .widgets import ConversationTimeline
12
13
 
13
14
 
14
15
  def composer_height(text):
@@ -33,20 +34,13 @@ class AgentTextualApp(App):
33
34
  super().__init__(**kwargs)
34
35
  self.chat = chat
35
36
  self.mouse_mode_hint = mouse_mode_hint
36
- self.transcript_renderer = None
37
37
  self._live_delta_flush_scheduled = False
38
+ self.perf = TuiPerfMonitor()
39
+ self.stream_debug = TuiStreamDebugLog()
38
40
 
39
41
  def compose(self) -> ComposeResult:
40
42
  with Vertical(id="main"):
41
- yield SelectableRichLog(
42
- id="conversation",
43
- wrap=True,
44
- highlight=False,
45
- markup=True,
46
- on_scroll_top=self.load_older_transcript,
47
- on_resize=lambda: self.handle_terminal_resize(log_resized=True),
48
- right_padding=6,
49
- )
43
+ yield ConversationTimeline(id="conversation")
50
44
  yield Static("", id="status")
51
45
  yield TextArea(
52
46
  "",
@@ -59,29 +53,26 @@ class AgentTextualApp(App):
59
53
 
60
54
  def on_mount(self):
61
55
  self.chat.app = self
62
- self.transcript_renderer = TranscriptRenderer(self.chat, self)
56
+ self.chat.client.stream_debug = self.stream_debug
57
+ self.stream_debug.record("tui_mount")
63
58
  install_tui_cleanup_handlers()
64
59
  self.chat.attach()
65
60
  self.chat.client.start_events()
66
61
  self.refresh_status()
67
62
  self.resize_input()
68
63
  self.query_one("#input", TextArea).focus()
64
+ self.set_interval(1.0, self.write_perf_snapshot)
69
65
  self.write_info(f"Codex Agent client ready - {self.mouse_mode_hint}")
70
66
 
71
67
  def on_resize(self, event: Resize):
72
68
  self.handle_terminal_resize()
73
69
 
74
70
  def handle_terminal_resize(self, *, log_resized=False):
75
- if getattr(self, "transcript_renderer", None) is None:
76
- return
77
- log = self.query_one("#conversation", SelectableRichLog)
78
- if not log_resized:
79
- log.invalidate_layout()
80
- self.render_transcript(keep_scroll=True)
81
71
  self.resize_input()
82
72
  self.refresh_status()
83
73
 
84
74
  def on_unmount(self):
75
+ self.stream_debug.record("tui_unmount")
85
76
  clear_tui(pid=os.getpid())
86
77
 
87
78
  def action_quit(self):
@@ -113,7 +104,7 @@ class AgentTextualApp(App):
113
104
 
114
105
  def action_clear(self):
115
106
  self.chat.transcript.clear()
116
- self.query_one("#conversation", SelectableRichLog).clear()
107
+ self.conversation.clear_messages()
117
108
  self.write_info("screen cleared - session history preserved")
118
109
  self.refresh_status()
119
110
 
@@ -150,77 +141,113 @@ class AgentTextualApp(App):
150
141
  finally:
151
142
  self.call_from_thread(self.refresh_status)
152
143
 
144
+ @property
145
+ def conversation(self):
146
+ return self.query_one("#conversation", ConversationTimeline)
147
+
153
148
  def render_transcript(self, *, keep_scroll=False):
154
- self.transcript_renderer.render(keep_scroll=keep_scroll)
149
+ # The widget timeline is updated incrementally. This method remains as a
150
+ # compatibility shim for older call sites during the migration.
151
+ return None
155
152
 
156
153
  def begin_transcript_batch(self):
157
- self.transcript_renderer.begin_batch()
154
+ return None
158
155
 
159
156
  def end_transcript_batch(self):
160
- self.transcript_renderer.end_batch()
157
+ return None
161
158
 
162
159
  def load_older_transcript(self):
163
- self.transcript_renderer.load_older()
160
+ return None
164
161
 
165
162
  def write_user(self, prompt):
166
163
  self.chat.transcript.append_user(prompt, self.chat.username())
167
- self.render_transcript()
164
+ self.conversation.add_user(prompt, title=self.chat.username())
168
165
 
169
166
  def ensure_agent_role(self, name=None):
170
- if self.chat.transcript.ensure_agent(name or self.chat.agent_name()):
171
- self.render_transcript()
167
+ self.chat.transcript.ensure_agent(name or self.chat.agent_name())
168
+ return self.conversation.ensure_active_assistant(title=name or self.chat.agent_name())
172
169
 
173
170
  def write_agent_start(self, name):
174
171
  self.chat.begin_turn_rendering()
175
172
 
176
173
  def start_agent_step(self):
177
174
  self.chat.transcript.begin_step()
178
- self.render_transcript()
179
175
 
180
176
  def write_agent_delta(self, delta):
177
+ self.stream_debug.record("ui_render_delta_begin", delta_len=len(str(delta or "")))
178
+ self.perf.count("write_agent_delta")
179
+ self.perf.count("write_agent_delta_chars", len(str(delta or "")))
180
+ with self.perf.timed("write_agent_delta"):
181
+ self.ensure_agent_role()
182
+ self.chat.transcript.apply_agent_delta(delta)
183
+ self.chat.transcript.step_had_content = True
184
+ self.conversation.append_assistant_delta(delta, title=self.chat.agent_name())
185
+ self.stream_debug.record("ui_render_delta_done", delta_len=len(str(delta or "")), live_chars=len(self.conversation._active_assistant_text))
186
+
187
+ def write_agent_message(self, content):
181
188
  self.ensure_agent_role()
182
- previous_text = self.chat.transcript.agent_buffer
183
- self.chat.transcript.apply_agent_delta(delta)
184
- self.chat.transcript.step_had_content = True
185
- if not self.transcript_renderer.update_last_agent(delta, previous_text):
186
- self.render_transcript()
189
+ self.chat.transcript.agent_buffer = str(content or "")
190
+ if self.chat.transcript.entries and self.chat.transcript.entries[-1].kind == "agent":
191
+ self.chat.transcript.entries[-1].text = self.chat.transcript.agent_buffer
192
+ self.conversation.add_assistant_message(content, title=self.chat.agent_name())
187
193
 
188
194
  def schedule_agent_delta_flush(self):
195
+ self.perf.count("schedule_agent_delta_flush")
189
196
  if self._live_delta_flush_scheduled:
190
197
  return
191
198
  self._live_delta_flush_scheduled = True
192
199
  self.set_timer(self.chat.live_delta.interval, self.flush_agent_delta)
193
200
 
194
201
  def flush_agent_delta(self):
195
- self._live_delta_flush_scheduled = False
196
- delta = self.chat.live_delta.pop()
197
- if delta:
198
- self.write_agent_delta(delta)
202
+ self.perf.count("flush_agent_delta")
203
+ with self.perf.timed("flush_agent_delta"):
204
+ self._live_delta_flush_scheduled = False
205
+ delta = self.chat.live_delta.pop()
206
+ if delta:
207
+ self.write_agent_delta(delta)
199
208
 
200
209
  def write_tool(self, name, call_id=None, status=None):
201
210
  self.flush_agent_delta()
202
- self.ensure_agent_role()
211
+ self.conversation.commit_active_assistant_preview()
203
212
  self.chat.transcript.append_tool(name, call_id=call_id, status=status)
204
- self.render_transcript()
213
+ self.conversation.add_tool(name, call_id=call_id, status=status)
205
214
 
206
215
  def write_slash_command(self, name, args="", status=None):
207
216
  self.flush_agent_delta()
208
- self.ensure_agent_role()
217
+ self.conversation.commit_active_assistant_preview()
209
218
  self.chat.transcript.append_slash_command(name, args=args, status=status)
210
- self.render_transcript()
219
+ self.conversation.add_slash_command(name, args=args, status=status)
211
220
 
212
221
  def end_agent_step(self):
213
222
  self.flush_agent_delta()
223
+ self.conversation.commit_active_assistant_preview()
214
224
  self.chat.transcript.end_step()
215
- self.render_transcript()
225
+ self.conversation.add_step_separator()
216
226
 
217
227
  def write_info(self, text):
218
228
  self.chat.transcript.append_info(text)
219
- self.render_transcript()
229
+ self.conversation.add_info(text)
220
230
 
221
231
  def write_error(self, text):
222
232
  self.chat.transcript.append_error(text)
223
- self.render_transcript()
233
+ self.conversation.add_error(text)
224
234
 
225
235
  def refresh_status(self):
226
- self.query_one("#status", Static).update(self.chat.status_renderable())
236
+ self.perf.count("refresh_status")
237
+ with self.perf.timed("refresh_status"):
238
+ self.query_one("#status", Static).update(self.chat.status_renderable())
239
+
240
+ def write_perf_snapshot(self):
241
+ client = self.chat.client
242
+ queue_obj = getattr(client, "_local_event_queue", None)
243
+ extra = {
244
+ "entries": len(self.chat.transcript.entries),
245
+ "live_buffer_chars": len(self.conversation._active_assistant_text),
246
+ "pending_live_delta_chars": len(self.chat.live_delta.text),
247
+ "last_event_sequence": getattr(client, "_last_event_sequence", None),
248
+ "event_queue_size": queue_obj.qsize() if queue_obj is not None else None,
249
+ "event_stream_connected": getattr(client, "_event_stream_connected", None),
250
+ "live_delta_flush_scheduled": self._live_delta_flush_scheduled,
251
+ }
252
+ self.perf.write_snapshot(extra=extra)
253
+ self.stream_debug.record("tui_heartbeat", **extra)
@@ -47,7 +47,7 @@ class Chat:
47
47
  """Textual-based chat UI client for a codex-agent server."""
48
48
 
49
49
  def __init__(self, client=None, base_url="http://127.0.0.1:8765"):
50
- self.client = client or AgentClient(base_url=base_url)
50
+ self.client = client or AgentClient(base_url=base_url, compact_events=True)
51
51
  self.app = None
52
52
  self.view = TuiAppBridge(self)
53
53
  self.deduper = DisplayDeduper()
@@ -233,6 +233,9 @@ class Chat:
233
233
  message = event.get("message") or {}
234
234
  if message.get("type") == "user_message":
235
235
  self.view.write_user(str(message.get("content") or ""))
236
+ elif message.get("type") == "assistant_message":
237
+ self._flush_live_delta()
238
+ self.view.write_agent_message(str(message.get("content") or ""))
236
239
  self.view.refresh_status()
237
240
 
238
241
  def _on_assistant_turn_start(self, event):
@@ -0,0 +1,73 @@
1
+ import json
2
+ import os
3
+ import time
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
+
7
+
8
+ class TuiPerfMonitor:
9
+ """Tiny in-process profiler for diagnosing TUI stream stalls.
10
+
11
+ The monitor keeps only counters and cumulative timings in memory, then writes
12
+ an atomic JSON snapshot. It is intentionally small enough to leave enabled
13
+ during local debugging without needing ptrace/py-spy access.
14
+ """
15
+
16
+ def __init__(self, path=None):
17
+ self.path = Path(path or os.path.expanduser("~/.codex-agent/tui-perf.json"))
18
+ self.started_at = time.monotonic()
19
+ self.reset_window()
20
+
21
+ def reset_window(self):
22
+ self.window_started_at = time.monotonic()
23
+ self.counts = {}
24
+ self.timings = {}
25
+ self.last = {}
26
+
27
+ def count(self, name, value=1):
28
+ self.counts[name] = self.counts.get(name, 0) + value
29
+
30
+ def set_last(self, name, value):
31
+ self.last[name] = value
32
+
33
+ @contextmanager
34
+ def timed(self, name):
35
+ start = time.perf_counter()
36
+ try:
37
+ yield
38
+ finally:
39
+ elapsed = time.perf_counter() - start
40
+ total, count, max_elapsed = self.timings.get(name, (0.0, 0, 0.0))
41
+ self.timings[name] = (total + elapsed, count + 1, max(max_elapsed, elapsed))
42
+
43
+ def snapshot(self, extra=None):
44
+ now = time.monotonic()
45
+ window_seconds = max(1e-9, now - self.window_started_at)
46
+ timings = {}
47
+ for name, (total, count, max_elapsed) in self.timings.items():
48
+ timings[name] = {
49
+ "count": count,
50
+ "total_ms": round(total * 1000, 3),
51
+ "avg_ms": round((total / count) * 1000, 3) if count else 0.0,
52
+ "max_ms": round(max_elapsed * 1000, 3),
53
+ }
54
+ return {
55
+ "pid": os.getpid(),
56
+ "uptime_seconds": round(now - self.started_at, 3),
57
+ "window_seconds": round(window_seconds, 3),
58
+ "rates_per_second": {key: round(value / window_seconds, 3) for key, value in self.counts.items()},
59
+ "counts": dict(self.counts),
60
+ "timings": timings,
61
+ "last": dict(self.last),
62
+ "extra": dict(extra or {}),
63
+ }
64
+
65
+ def write_snapshot(self, extra=None, *, reset=True):
66
+ self.path.parent.mkdir(parents=True, exist_ok=True)
67
+ payload = self.snapshot(extra=extra)
68
+ tmp_path = self.path.with_suffix(self.path.suffix + ".tmp")
69
+ tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
70
+ tmp_path.replace(self.path)
71
+ if reset:
72
+ self.reset_window()
73
+ return payload
@@ -1,5 +1,13 @@
1
1
  class LiveDeltaBuffer:
2
- def __init__(self, interval=1 / 60):
2
+ """Coalesce live text deltas before touching the Textual render loop.
3
+
4
+ Human-readable terminal text does not benefit from display-refresh-rate
5
+ updates, and high-frequency flushes can overwhelm Textual during bursty
6
+ model streams. Twenty frames per second keeps the UI feeling live while
7
+ leaving enough breathing room for input, health checks, and repaint work.
8
+ """
9
+
10
+ def __init__(self, interval=1 / 20):
3
11
  self.interval = interval
4
12
  self.text = ""
5
13
  self.flush_requested = False
@@ -59,8 +67,29 @@ class TuiAppBridge:
59
67
  return None
60
68
  if self.app is None:
61
69
  return None
70
+ perf = getattr(self.app, "perf", None)
71
+ if perf is not None:
72
+ perf.count("bridge_call_from_thread")
73
+ perf.count(f"bridge_call_from_thread.{method}")
74
+ stream_debug = getattr(self.app, "stream_debug", None)
75
+ if stream_debug is not None:
76
+ stream_debug.record("bridge_schedule", method=method, args_count=len(args))
62
77
  callback = getattr(self.app, method)
63
- return self.app.call_from_thread(callback, *args)
78
+
79
+ def traced_callback(*callback_args):
80
+ if stream_debug is not None:
81
+ stream_debug.record("ui_callback_begin", method=method, args_count=len(callback_args))
82
+ try:
83
+ result = callback(*callback_args)
84
+ if stream_debug is not None:
85
+ stream_debug.record("ui_callback_done", method=method, args_count=len(callback_args))
86
+ return result
87
+ except Exception as exc:
88
+ if stream_debug is not None:
89
+ stream_debug.record("ui_callback_error", method=method, error=str(exc), args_count=len(callback_args))
90
+ raise
91
+
92
+ return self.app.call_from_thread(traced_callback, *args)
64
93
 
65
94
  def refresh_status(self):
66
95
  return self.call("refresh_status", skip_during_replay=True)
@@ -92,6 +121,9 @@ class TuiAppBridge:
92
121
  def write_agent_delta(self, delta):
93
122
  return self.call("write_agent_delta", delta)
94
123
 
124
+ def write_agent_message(self, content):
125
+ return self.call("write_agent_message", content)
126
+
95
127
  def write_tool(self, name, call_id=None, status=None):
96
128
  return self.call("write_tool", name, call_id, status)
97
129
 
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+
9
+
10
+ class TuiStreamDebugLog:
11
+ """Append-only black-box log for intermittent TUI stream stalls.
12
+
13
+ The file is intentionally outside the Textual render path and is flushed on
14
+ every record. When the UI stops updating but the process stays alive, the
15
+ last records should reveal whether events stopped at SSE receive, queue,
16
+ dispatch, bridge scheduling, or actual widget rendering.
17
+ """
18
+
19
+ def __init__(self, path=None, *, max_bytes=5_000_000):
20
+ self.path = Path(path or os.path.expanduser("~/.codex-agent/tui-stream-debug.jsonl"))
21
+ self.max_bytes = int(max_bytes or 0)
22
+ self._lock = threading.Lock()
23
+ self._started_at = time.monotonic()
24
+
25
+ def record(self, stage, event=None, **fields):
26
+ payload = {
27
+ "ts": round(time.time(), 6),
28
+ "monotonic": round(time.monotonic(), 6),
29
+ "uptime": round(time.monotonic() - self._started_at, 6),
30
+ "pid": os.getpid(),
31
+ "thread": threading.current_thread().name,
32
+ "stage": str(stage),
33
+ }
34
+ payload.update(self.event_fields(event))
35
+ payload.update(fields)
36
+ line = json.dumps(payload, ensure_ascii=False, default=str, sort_keys=True)
37
+ with self._lock:
38
+ self.path.parent.mkdir(parents=True, exist_ok=True)
39
+ self._trim_if_needed()
40
+ with self.path.open("a", encoding="utf-8") as handle:
41
+ handle.write(line + "\n")
42
+ handle.flush()
43
+ return payload
44
+
45
+ def event_fields(self, event):
46
+ if not isinstance(event, dict):
47
+ return {}
48
+ payload = {
49
+ "event_type": event.get("type"),
50
+ "event_id": event.get("id"),
51
+ "sequence": event.get("sequence"),
52
+ }
53
+ if "delta" in event:
54
+ payload["delta_len"] = len(str(event.get("delta") or ""))
55
+ if "content" in event:
56
+ payload["content_len"] = len(str(event.get("content") or ""))
57
+ payload["content_truncated"] = bool(event.get("content_truncated"))
58
+ payload["content_full_length"] = event.get("content_full_length")
59
+ message = event.get("message")
60
+ if isinstance(message, dict):
61
+ payload["message_type"] = message.get("type")
62
+ payload["message_content_len"] = len(str(message.get("content") or ""))
63
+ tool_call = event.get("tool_call")
64
+ if isinstance(tool_call, dict):
65
+ payload["tool_name"] = tool_call.get("name")
66
+ payload["tool_call_id"] = tool_call.get("call_id")
67
+ if "error" in event:
68
+ payload["error"] = str(event.get("error") or "")[:500]
69
+ return payload
70
+
71
+ def _trim_if_needed(self):
72
+ if self.max_bytes <= 0 or not self.path.exists():
73
+ return
74
+ try:
75
+ if self.path.stat().st_size <= self.max_bytes:
76
+ return
77
+ data = self.path.read_bytes()[-self.max_bytes // 2:]
78
+ first_newline = data.find(b"\n")
79
+ if first_newline >= 0:
80
+ data = data[first_newline + 1:]
81
+ self.path.write_bytes(data)
82
+ except Exception:
83
+ # Debug logging must never break the TUI.
84
+ return