codex-agent-framework 0.1.55__tar.gz → 0.1.57__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.57}/CHANGELOG.md +21 -0
  2. {codex_agent_framework-0.1.55/codex_agent_framework.egg-info → codex_agent_framework-0.1.57}/PKG-INFO +1 -1
  3. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/client.py +34 -4
  4. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/server/app.py +26 -3
  5. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/stream_utils.py +42 -75
  6. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/app.py +72 -45
  7. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/chat.py +4 -1
  8. codex_agent_framework-0.1.57/codex_agent/tui/perf.py +73 -0
  9. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/state.py +34 -2
  10. codex_agent_framework-0.1.57/codex_agent/tui/stream_debug.py +84 -0
  11. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/style.py +49 -0
  12. codex_agent_framework-0.1.57/codex_agent/tui/widgets.py +248 -0
  13. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/utils.py +30 -57
  14. codex_agent_framework-0.1.57/codex_agent/version.py +1 -0
  15. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57/codex_agent_framework.egg-info}/PKG-INFO +1 -1
  16. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/SOURCES.txt +3 -0
  17. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/pyproject.toml +1 -1
  18. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_chat.py +47 -58
  19. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_server.py +50 -0
  20. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_utils.py +87 -1
  21. codex_agent_framework-0.1.55/codex_agent/version.py +0 -1
  22. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/LICENSE +0 -0
  23. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/MANIFEST.in +0 -0
  24. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/README.md +0 -0
  25. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/__init__.py +0 -0
  26. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/__main__.py +0 -0
  27. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/agent.py +0 -0
  28. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/agent_runtime.py +0 -0
  29. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/ai.py +0 -0
  30. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/browser.py +0 -0
  31. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_commands.py +0 -0
  32. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/__init__.py +0 -0
  33. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  34. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  35. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  36. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  37. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  38. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  39. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/system.py +0 -0
  40. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/vision.py +0 -0
  41. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/context/__init__.py +0 -0
  42. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  43. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  44. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  45. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  46. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  47. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  48. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/files/tools.py +0 -0
  49. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  50. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  51. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  52. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  53. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  54. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  55. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  56. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  57. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  58. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  59. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  60. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  61. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  62. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  63. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  64. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  65. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  66. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  67. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  68. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  69. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  70. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  71. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  72. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  73. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/web_search/__init__.py +0 -0
  74. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/__init__.py +0 -0
  75. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/headless.py +0 -0
  76. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/main.py +0 -0
  77. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/output.py +0 -0
  78. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/root.py +0 -0
  79. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/runner.py +0 -0
  80. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/command.py +0 -0
  81. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/config.py +0 -0
  82. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/context.py +0 -0
  83. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/desktop.py +0 -0
  84. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/event.py +0 -0
  85. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/__init__.py +0 -0
  86. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/default_gitignore +0 -0
  87. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/get_text.py +0 -0
  88. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/simpler_get_text.py +0 -0
  89. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/hooks.py +0 -0
  90. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/image.py +0 -0
  91. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/latex.py +0 -0
  92. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/mainloop.py +0 -0
  93. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/message.py +0 -0
  94. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/plugin.py +0 -0
  95. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/prompts/system_prompt.txt +0 -0
  96. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/provider.py +0 -0
  97. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/runtime.py +0 -0
  98. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/scheduler.py +0 -0
  99. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  100. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/server/__init__.py +0 -0
  101. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/server/core.py +0 -0
  102. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/service.py +0 -0
  103. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/sessions.py +0 -0
  104. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/status.py +0 -0
  105. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/stream.py +0 -0
  106. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tool.py +0 -0
  107. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tray.py +0 -0
  108. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/__init__.py +0 -0
  109. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/lifecycle.py +0 -0
  110. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/log.py +0 -0
  111. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/status_bar.py +0 -0
  112. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/transcript.py +0 -0
  113. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/worker.py +0 -0
  114. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  115. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  116. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/requires.txt +0 -0
  117. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/top_level.txt +0 -0
  118. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/dependencies.txt +0 -0
  119. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/setup.cfg +0 -0
  120. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent.py +0 -0
  121. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_audio_hooks.py +0 -0
  122. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_commands.py +0 -0
  123. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_context.py +0 -0
  124. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_desktop.py +0 -0
  125. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_environment.py +0 -0
  126. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_image_generation.py +0 -0
  127. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_observe.py +0 -0
  128. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_plugins.py +0 -0
  129. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_providers_config.py +0 -0
  130. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_realtime.py +0 -0
  131. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_response.py +0 -0
  132. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_runtime.py +0 -0
  133. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_sessions.py +0 -0
  134. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_subagents.py +0 -0
  135. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_tools.py +0 -0
  136. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_turns.py +0 -0
  137. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_ai.py +0 -0
  138. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_browser.py +0 -0
  139. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_builtin_config.py +0 -0
  140. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_cli.py +0 -0
  141. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_cli_headless.py +0 -0
  142. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_cli_root.py +0 -0
  143. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_client.py +0 -0
  144. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_events.py +0 -0
  145. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_get_text_browser.py +0 -0
  146. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_hooks.py +0 -0
  147. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_image_message.py +0 -0
  148. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_local_desktop.py +0 -0
  149. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_memory.py +0 -0
  150. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_messages.py +0 -0
  151. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_planner.py +0 -0
  152. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_scheduler.py +0 -0
  153. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_service.py +0 -0
  154. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_tui.py +0 -0
  155. {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_worker.py +0 -0
@@ -4,6 +4,27 @@ 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.57] - 2026-05-17
8
+ ### Fixed
9
+ - Fix stream helper fan-out edge cases so `Tee`, `Splitter`, and `MappingStreamSplitter` readers keep their own queues and reusable mapping splitters do not redirect old readers to new streams.
10
+ - Make `utils.Logger` create its parent directory before truncating the log file so first use works on fresh paths.
11
+
12
+ ### Changed
13
+ - Simplify small utility helpers for line-preserving truncation, message packing, and fenced stream pattern construction while preserving their existing behavior.
14
+
15
+ ### Tests
16
+ - Add focused regression coverage for the stream helpers, logger initialization, and utility formatting/truncation helpers; validate with the full offline CI suite (580 passed).
17
+
18
+ ## [0.1.56] - 2026-05-17
19
+ ### Changed
20
+ - 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.
21
+ - 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.
22
+ - 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.
23
+ - Preserve the previous sparse terminal look with role headers, compact tool/info/error rows, a right-aligned step marker, and sticky-only autoscroll.
24
+
25
+ ### Tests
26
+ - Validate the TUI/server/client changes with targeted chat/client/server coverage and the full offline CI suite (570 passed).
27
+
7
28
  ## [0.1.55] - 2026-05-17
8
29
  ### Fixed
9
30
  - 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.57
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
 
@@ -53,17 +53,16 @@ class Tee:
53
53
  for k in self.queues:
54
54
  self.queues[k].put(END)
55
55
 
56
- def tee(self,stream, n=2):
57
- self.queues={k:Queue() for k in range(n)}
58
- self.thread=Thread(target=self.target, args=(stream,),kwargs=dict(n=n),daemon=True)
56
+ def tee(self, stream, n=2):
57
+ self.queues = {k: Queue() for k in range(n)}
58
+ self.thread = Thread(target=self.target, args=(stream,), daemon=True)
59
59
  self.thread.start()
60
- readers={}
61
- for k in self.queues:
62
- def reader():
63
- while (processed_chunk:=self.queues[k].get()) is not END:
64
- yield processed_chunk
65
- readers[k]=reader()
66
- return tuple(readers.values())
60
+
61
+ def reader(queue):
62
+ while (processed_chunk := queue.get()) is not END:
63
+ yield processed_chunk
64
+
65
+ return tuple(reader(queue) for queue in self.queues.values())
67
66
 
68
67
  class Splitter:
69
68
 
@@ -86,17 +85,16 @@ class Splitter:
86
85
  self.queue_false.put(END)
87
86
 
88
87
  def split(self, stream):
89
- self.queue_true=Queue()
90
- self.queue_false=Queue()
91
- self.thread=Thread(target=self.target, args=(stream,),daemon=True)
88
+ self.queue_true = Queue()
89
+ self.queue_false = Queue()
90
+ self.thread = Thread(target=self.target, args=(stream,), daemon=True)
92
91
  self.thread.start()
93
- def reader_true():
94
- while (chunk:=self.queue_true.get()) is not END:
95
- yield chunk
96
- def reader_false():
97
- while (chunk:=self.queue_false.get()) is not END:
92
+
93
+ def reader(queue):
94
+ while (chunk := queue.get()) is not END:
98
95
  yield chunk
99
- return reader_true(),reader_false()
96
+
97
+ return reader(self.queue_true), reader(self.queue_false)
100
98
 
101
99
  class Streamer:
102
100
 
@@ -159,61 +157,27 @@ class StreamLogger(Streamer):
159
157
  with open(self.file,'a', encoding='utf-8') as f:
160
158
  f.write(output)
161
159
 
162
- def fenced(start: Union[str,regex.Pattern,re.Pattern], end: Union[str,regex.Pattern,re.Pattern], flags=0) -> regex.Pattern:
163
- r"""
164
- Generates a regex pattern that matches text enclosed between a start and an end delimiter,
165
- The inner part is captured as the 'content' group (default).
166
- There can be other captured groups defined in the start and end patterns.
167
- The end delimiter can include backreferences to captured groups from the opening fence into placeholders in the closing fence.
168
-
169
- The closing fence pattern may include placeholders in the form of "#1#" for the first captured group,
170
- "#2#" for the second, or "#name#" for a named capturing group; these will be replaced by the appropriate
171
- backreference. This mechanism is completely generic and does not enforce any specific delimiter shapes.
172
-
173
- Example:
174
- # Using a numeric placeholder:
175
- pattern = fenced(r"START (\w+)", r"END #1#", flags=regex.DOTALL)
176
- text = "START content END content"
177
- match = pattern.match(text)
178
- if match:
179
- print(match.group())
180
-
181
- # Using a named capturing group:
182
- pattern = fenced(r"BEGIN (?P<tag>\w+)", r"FIN #tag#", flags=regex.DOTALL)
183
- text = "BEGIN example FIN example"
184
- match = pattern.match(text)
185
- if match:
186
- print(match.group())
187
-
188
- :param start: Opening delimiter (str or regex.Pattern). May contain capturing groups.
189
- :param end: Closing delimiter (str or regex.Pattern) which may include placeholders (e.g., "#1#", "#name#").
190
- :param flags: Regex flags to modify behavior (e.g., regex.DOTALL, regex.MULTILINE).
191
- :return: A compiled regex pattern.
160
+ def fenced(start: Union[str, regex.Pattern, re.Pattern], end: Union[str, regex.Pattern, re.Pattern], flags=0) -> regex.Pattern:
161
+ r"""Compile a fenced-content pattern with the inner text captured as ``content``.
162
+
163
+ ``end`` may reference capture groups from ``start`` with ``#1#`` or
164
+ ``#name#`` placeholders, which are converted to regex backreferences.
192
165
  """
193
- def prepare_pattern(pattern):
194
- if isinstance(pattern, (regex.Pattern,re.Pattern)):
195
- return pattern.pattern
166
+ def pattern_source(pattern):
196
167
  if isinstance(pattern, str):
197
168
  return pattern
198
- raise ValueError("`start` and `end` must be either a string or a regex Pattern object")
199
-
200
- start_regex = prepare_pattern(start)
201
- end_regex = prepare_pattern(end)
202
-
203
- # Replace placeholders in end pattern with regex backrefs
204
- def replace_placeholder(match):
169
+ if isinstance(pattern, (regex.Pattern, re.Pattern)):
170
+ return pattern.pattern
171
+ raise ValueError("`start` and `end` must be strings or regex Pattern objects")
172
+
173
+ def placeholder_to_backref(match):
205
174
  key = match.group(1)
206
- if key.isdigit():
207
- return rf"\{key}"
208
- else:
209
- return rf"(?P={key})"
210
- end_regex = regex.sub(r"#(\w+)#", replace_placeholder, end_regex)
211
-
212
- # Named content group
213
- inner = rf'(?P<content>(?:(?!{end_regex})[\s\S])*)'
214
-
215
- full_pattern = rf'{start_regex}{inner}(?:{end_regex}|$)'
216
- return regex.compile(full_pattern, flags)
175
+ return rf"\{key}" if key.isdigit() else rf"(?P={key})"
176
+
177
+ start_regex = pattern_source(start)
178
+ end_regex = regex.sub(r"#(\w+)#", placeholder_to_backref, pattern_source(end))
179
+ inner = rf"(?P<content>(?:(?!{end_regex})[\s\S])*)"
180
+ return regex.compile(rf"{start_regex}{inner}(?:{end_regex}|$)", flags)
217
181
 
218
182
  def is_compiled_pattern(pattern):
219
183
  if isinstance(pattern, (re.Pattern,regex.Pattern)):
@@ -528,16 +492,19 @@ class MappingStreamSplitter:
528
492
  def maybe_init_readers(self, data):
529
493
  for key in data.keys():
530
494
  if key not in self.readers:
531
- self.queues[key]=Queue()
532
- def reader(key=key):
533
- while (value:=self.queues[key].get()) is not END:
495
+ queue = self.queues[key] = Queue()
496
+
497
+ def reader(queue=queue):
498
+ while (value := queue.get()) is not END:
534
499
  if isinstance(value, StreamError):
535
500
  raise value.error
536
501
  yield value
537
- self.readers[key]=reader()
502
+
503
+ self.readers[key] = reader()
538
504
 
539
505
  def process(self,stream):
540
- self.queues=dict()
506
+ self.readers = {}
507
+ self.queues = {}
541
508
  self.done=False
542
509
  if self.defaults:
543
510
  self.maybe_init_readers(self.defaults)
@@ -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):