codex-agent-framework 0.1.28__tar.gz → 0.1.29__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 (138) hide show
  1. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/CHANGELOG.md +13 -0
  2. {codex_agent_framework-0.1.28/codex_agent_framework.egg-info → codex_agent_framework-0.1.29}/PKG-INFO +1 -1
  3. codex_agent_framework-0.1.29/codex_agent/tui/app.py +211 -0
  4. codex_agent_framework-0.1.29/codex_agent/tui/chat.py +343 -0
  5. codex_agent_framework-0.1.29/codex_agent/tui/log.py +225 -0
  6. codex_agent_framework-0.1.29/codex_agent/tui/state.py +148 -0
  7. codex_agent_framework-0.1.29/codex_agent/tui/status_bar.py +86 -0
  8. codex_agent_framework-0.1.29/codex_agent/tui/style.py +56 -0
  9. codex_agent_framework-0.1.29/codex_agent/tui/transcript.py +337 -0
  10. codex_agent_framework-0.1.29/codex_agent/version.py +1 -0
  11. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29/codex_agent_framework.egg-info}/PKG-INFO +1 -1
  12. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/SOURCES.txt +6 -0
  13. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/pyproject.toml +1 -1
  14. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_chat.py +206 -106
  15. codex_agent_framework-0.1.28/codex_agent/tui/chat.py +0 -1293
  16. codex_agent_framework-0.1.28/codex_agent/version.py +0 -1
  17. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/LICENSE +0 -0
  18. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/MANIFEST.in +0 -0
  19. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/README.md +0 -0
  20. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/__init__.py +0 -0
  21. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/__main__.py +0 -0
  22. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/agent.py +0 -0
  23. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/agent_runtime.py +0 -0
  24. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/ai.py +0 -0
  25. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/browser.py +0 -0
  26. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_commands.py +0 -0
  27. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/__init__.py +0 -0
  28. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  29. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  30. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  31. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  32. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  33. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  34. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/system.py +0 -0
  35. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/vision.py +0 -0
  36. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/context/__init__.py +0 -0
  37. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  38. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  39. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  40. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  41. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  42. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  43. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/files/tools.py +0 -0
  44. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  45. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  46. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  47. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  48. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  49. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  50. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  51. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  52. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  53. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  54. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  55. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  56. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  57. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  58. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  59. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  60. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  61. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  62. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  63. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  64. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  65. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  66. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  67. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  68. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/__init__.py +0 -0
  69. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/headless.py +0 -0
  70. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/main.py +0 -0
  71. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/output.py +0 -0
  72. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/root.py +0 -0
  73. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/runner.py +0 -0
  74. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/client.py +0 -0
  75. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/command.py +0 -0
  76. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/config.py +0 -0
  77. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/context.py +0 -0
  78. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/desktop.py +0 -0
  79. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/event.py +0 -0
  80. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/__init__.py +0 -0
  81. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/default_gitignore +0 -0
  82. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/get_text.py +0 -0
  83. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/simpler_get_text.py +0 -0
  84. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/hooks.py +0 -0
  85. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/image.py +0 -0
  86. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/latex.py +0 -0
  87. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/mainloop.py +0 -0
  88. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/message.py +0 -0
  89. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/plugin.py +0 -0
  90. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/prompts/system_prompt.txt +0 -0
  91. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/provider.py +0 -0
  92. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/runtime.py +0 -0
  93. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/scheduler.py +0 -0
  94. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  95. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/server/__init__.py +0 -0
  96. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/server/app.py +0 -0
  97. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/server/core.py +0 -0
  98. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/service.py +0 -0
  99. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/sessions.py +0 -0
  100. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/status.py +0 -0
  101. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/stream.py +0 -0
  102. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/stream_utils.py +0 -0
  103. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tool.py +0 -0
  104. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tray.py +0 -0
  105. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tui/__init__.py +0 -0
  106. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tui/lifecycle.py +0 -0
  107. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/utils.py +0 -0
  108. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/worker.py +0 -0
  109. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  110. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  111. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/requires.txt +0 -0
  112. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/top_level.txt +0 -0
  113. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/dependencies.txt +0 -0
  114. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/setup.cfg +0 -0
  115. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_agent.py +0 -0
  116. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_agent_runtime.py +0 -0
  117. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_ai.py +0 -0
  118. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_browser.py +0 -0
  119. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_builtin_config.py +0 -0
  120. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_cli.py +0 -0
  121. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_cli_headless.py +0 -0
  122. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_cli_root.py +0 -0
  123. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_client.py +0 -0
  124. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_events.py +0 -0
  125. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_get_text_browser.py +0 -0
  126. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_hooks.py +0 -0
  127. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_image_message.py +0 -0
  128. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_local_desktop.py +0 -0
  129. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_memory.py +0 -0
  130. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_messages.py +0 -0
  131. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_planner.py +0 -0
  132. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_scheduler.py +0 -0
  133. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_server.py +0 -0
  134. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_service.py +0 -0
  135. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_tray.py +0 -0
  136. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_tui.py +0 -0
  137. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_utils.py +0 -0
  138. {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_worker.py +0 -0
@@ -4,6 +4,19 @@ 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.29] - 2026-05-15
8
+ ### Changed
9
+ - Split the Textual TUI implementation into focused modules for app wiring, transcript rendering, status display, styling, log windowing, and render state.
10
+ - Replace ad hoc TUI app calls and duplicate display tracking with a typed bridge and centralized deduplication state.
11
+ - Move terminal resize handling into the Textual app so transcript, input sizing, layout cache, and status refresh stay coordinated.
12
+
13
+ ### Fixed
14
+ - Invalidate selectable log layout metrics cleanly on terminal resize to keep wrapped transcript rendering accurate.
15
+ - Keep TUI turn rendering state scoped to the active turn without leaking tool or slash-command display markers.
16
+
17
+ ### Tests
18
+ - Update TUI/chat regression coverage for the extracted bridge/state modules and resize handling; the current suite validates at 574 passed.
19
+
7
20
  ## [0.1.28] - 2026-05-15
8
21
  ### Added
9
22
  - Add `StreamManager` as a dedicated stream-processing manager and expose it through the package API.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.28
3
+ Version: 0.1.29
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -0,0 +1,211 @@
1
+ import os
2
+
3
+ from textual.app import App, ComposeResult
4
+ from textual.containers import Vertical
5
+ from textual.events import Resize
6
+ from textual.widgets import Footer, Static, TextArea
7
+
8
+ from .lifecycle import clear_tui, install_tui_cleanup_handlers
9
+ from .log import SelectableRichLog
10
+ from .style import CHAT_CSS
11
+ from .transcript import TranscriptRenderer
12
+
13
+
14
+ def composer_height(text):
15
+ useful_lines = max(1, min(10, str(text or "").count("\n") + 1))
16
+ return useful_lines + 2
17
+
18
+
19
+ class AgentTextualApp(App):
20
+ CSS = CHAT_CSS
21
+ TITLE = "Codex Agent"
22
+ BINDINGS = [
23
+ ("ctrl+c", "interrupt", "Interrupt"),
24
+ ("ctrl+l", "clear", "Clear"),
25
+ ("ctrl+q", "quit", "Quit"),
26
+ ("ctrl+s", "submit", "Submit"),
27
+ ("alt+enter", "submit", "Submit"),
28
+ ("ctrl+enter", "submit", "Submit"),
29
+ ("f2", "submit", "Submit"),
30
+ ]
31
+
32
+ def __init__(self, chat, mouse_mode_hint, **kwargs):
33
+ super().__init__(**kwargs)
34
+ self.chat = chat
35
+ self.mouse_mode_hint = mouse_mode_hint
36
+ self.transcript_renderer = None
37
+ self._live_delta_flush_scheduled = False
38
+
39
+ def compose(self) -> ComposeResult:
40
+ 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
+ )
50
+ yield Static("", id="status")
51
+ yield TextArea(
52
+ "",
53
+ placeholder=f"> message - Enter = newline - Ctrl+S/F2 = send - {self.mouse_mode_hint}",
54
+ id="input",
55
+ show_line_numbers=False,
56
+ soft_wrap=True,
57
+ )
58
+ yield Footer()
59
+
60
+ def on_mount(self):
61
+ self.chat.app = self
62
+ self.transcript_renderer = TranscriptRenderer(self.chat, self)
63
+ install_tui_cleanup_handlers()
64
+ self.chat.attach()
65
+ self.chat.client.start_events()
66
+ self.refresh_status()
67
+ self.resize_input()
68
+ self.query_one("#input", TextArea).focus()
69
+ self.write_info(f"Codex Agent client ready - {self.mouse_mode_hint}")
70
+
71
+ def on_resize(self, event: Resize):
72
+ self.handle_terminal_resize()
73
+
74
+ 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
+ self.resize_input()
82
+ self.refresh_status()
83
+
84
+ def on_unmount(self):
85
+ clear_tui(pid=os.getpid())
86
+
87
+ def action_quit(self):
88
+ clear_tui(pid=os.getpid())
89
+ self.chat.client.stop(timeout=0.2)
90
+ self.exit()
91
+
92
+ def action_submit(self):
93
+ input_widget = self.query_one("#input", TextArea)
94
+ prompt = input_widget.text
95
+ command = prompt.strip().lower()
96
+ if not command:
97
+ return
98
+ if command in ["exit", "quit", "/exit", "/quit"]:
99
+ self.exit()
100
+ return
101
+ if command == "/clear":
102
+ input_widget.load_text("")
103
+ self.action_clear()
104
+ return
105
+ input_widget.load_text("")
106
+ self.refresh_status()
107
+ self.run_worker(lambda: self._submit_prompt(prompt), thread=True)
108
+
109
+ def action_clear(self):
110
+ self.chat.transcript.clear()
111
+ self.query_one("#conversation", SelectableRichLog).clear()
112
+ self.write_info("screen cleared - session history preserved")
113
+ self.refresh_status()
114
+
115
+ def action_interrupt(self):
116
+ if self.screen.get_selected_text():
117
+ self.screen.action_copy_text()
118
+ return
119
+ result = self.chat.client.interrupt("keyboard_interrupt")
120
+ self.write_error(f"interrupt requested - {result.error}" if result.get("error") else "interrupt requested")
121
+
122
+ def on_text_area_changed(self, event: TextArea.Changed):
123
+ if event.text_area.id == "input":
124
+ self.resize_input()
125
+
126
+ def resize_input(self):
127
+ input_widget = self.query_one("#input", TextArea)
128
+ input_widget.styles.height = composer_height(input_widget.text)
129
+
130
+ def _submit_prompt(self, prompt):
131
+ try:
132
+ self.chat.client.submit_prompt(prompt)
133
+ except Exception as exc:
134
+ self.call_from_thread(self.write_error, str(exc))
135
+ finally:
136
+ self.call_from_thread(self.refresh_status)
137
+
138
+ def render_transcript(self, *, keep_scroll=False):
139
+ self.transcript_renderer.render(keep_scroll=keep_scroll)
140
+
141
+ def begin_transcript_batch(self):
142
+ self.transcript_renderer.begin_batch()
143
+
144
+ def end_transcript_batch(self):
145
+ self.transcript_renderer.end_batch()
146
+
147
+ def load_older_transcript(self):
148
+ self.transcript_renderer.load_older()
149
+
150
+ def write_user(self, prompt):
151
+ self.chat.transcript.append_user(prompt, self.chat.username())
152
+ self.render_transcript()
153
+
154
+ def ensure_agent_role(self, name=None):
155
+ if self.chat.transcript.ensure_agent(name or self.chat.agent_name()):
156
+ self.render_transcript()
157
+
158
+ def write_agent_start(self, name):
159
+ self.chat.begin_turn_rendering()
160
+
161
+ def start_agent_step(self):
162
+ self.chat.transcript.begin_step()
163
+ self.render_transcript()
164
+
165
+ def write_agent_delta(self, delta):
166
+ self.ensure_agent_role()
167
+ previous_text = self.chat.transcript.agent_buffer
168
+ self.chat.transcript.apply_agent_delta(delta)
169
+ self.chat.transcript.step_had_content = True
170
+ if not self.transcript_renderer.update_last_agent(delta, previous_text):
171
+ self.render_transcript()
172
+
173
+ def schedule_agent_delta_flush(self):
174
+ if self._live_delta_flush_scheduled:
175
+ return
176
+ self._live_delta_flush_scheduled = True
177
+ self.set_timer(self.chat.live_delta.interval, self.flush_agent_delta)
178
+
179
+ def flush_agent_delta(self):
180
+ self._live_delta_flush_scheduled = False
181
+ delta = self.chat.live_delta.pop()
182
+ if delta:
183
+ self.write_agent_delta(delta)
184
+
185
+ def write_tool(self, name, call_id=None, status=None):
186
+ self.flush_agent_delta()
187
+ self.ensure_agent_role()
188
+ self.chat.transcript.append_tool(name, call_id=call_id, status=status)
189
+ self.render_transcript()
190
+
191
+ def write_slash_command(self, name, args="", status=None):
192
+ self.flush_agent_delta()
193
+ self.ensure_agent_role()
194
+ self.chat.transcript.append_slash_command(name, args=args, status=status)
195
+ self.render_transcript()
196
+
197
+ def end_agent_step(self):
198
+ self.flush_agent_delta()
199
+ self.chat.transcript.end_step()
200
+ self.render_transcript()
201
+
202
+ def write_info(self, text):
203
+ self.chat.transcript.append_info(text)
204
+ self.render_transcript()
205
+
206
+ def write_error(self, text):
207
+ self.chat.transcript.append_error(text)
208
+ self.render_transcript()
209
+
210
+ def refresh_status(self):
211
+ self.query_one("#status", Static).update(self.chat.status_renderable())
@@ -0,0 +1,343 @@
1
+ import os
2
+ import sys
3
+
4
+ from modict import modict
5
+ from ..client import AgentClient
6
+ from ..event import (
7
+ AssistantStepEndEvent,
8
+ AssistantStepStartEvent,
9
+ AssistantTurnEndEvent,
10
+ AssistantTurnErrorEvent,
11
+ AssistantTurnStartEvent,
12
+ CompactionCompletedEvent,
13
+ MessageAddedEvent,
14
+ ResponseContentDeltaEvent,
15
+ ResponseDoneEvent,
16
+ ServerToolCallEvent,
17
+ SlashCommandDoneEvent,
18
+ SlashCommandStartEvent,
19
+ ToolCallDoneEvent,
20
+ ToolCallStartEvent,
21
+ )
22
+ from .app import AgentTextualApp
23
+ from .lifecycle import register_tui
24
+ from .state import DisplayDeduper, LiveDeltaBuffer, ReplayDeltaBuffer, TuiAppBridge
25
+ from .status_bar import StatusBar
26
+ from .transcript import Transcript
27
+
28
+
29
+ TUI_MOUSE_ENV = "CODEX_AGENT_TUI_MOUSE"
30
+ TRUE_ENV_VALUES = {"1", "true", "yes", "on"}
31
+ FALSE_ENV_VALUES = {"0", "false", "no", "off"}
32
+
33
+
34
+ def tui_mouse_enabled(value=None, default=True):
35
+ value = os.environ.get(TUI_MOUSE_ENV) if value is None else value
36
+ if value is None:
37
+ return default
38
+ normalized = str(value).strip().lower()
39
+ if normalized in TRUE_ENV_VALUES:
40
+ return True
41
+ if normalized in FALSE_ENV_VALUES:
42
+ return False
43
+ return default
44
+
45
+
46
+ class Chat:
47
+ """Textual-based chat UI client for a codex-agent server."""
48
+
49
+ def __init__(self, client=None, base_url="http://127.0.0.1:8765"):
50
+ self.client = client or AgentClient(base_url=base_url)
51
+ self.app = None
52
+ self.view = TuiAppBridge(self)
53
+ self.deduper = DisplayDeduper()
54
+ self.transcript = Transcript()
55
+ self._server_connected = False
56
+ self._server_error = ""
57
+ self._replaying_events = False
58
+ self.replay_delta = ReplayDeltaBuffer()
59
+ self.live_delta = LiveDeltaBuffer()
60
+
61
+ def attach(self):
62
+ self.client.on(AssistantTurnStartEvent, self._once(self._on_assistant_turn_start))
63
+ self.client.on(AssistantStepStartEvent, self._once(self._on_assistant_step_start))
64
+ self.client.on(AssistantStepEndEvent, self._once(self._on_assistant_step_end))
65
+ self.client.on(AssistantTurnEndEvent, self._once(self._on_assistant_turn_end))
66
+ self.client.on(AssistantTurnErrorEvent, self._once(self._on_assistant_turn_error))
67
+ self.client.on(CompactionCompletedEvent, self._once(self._on_compaction_completed))
68
+ self.client.on(MessageAddedEvent, self._once(self._on_message_added))
69
+ self.client.on(ResponseContentDeltaEvent, self._once(self._on_content_delta))
70
+ self.client.on(ResponseDoneEvent, self._once(self._on_response_done))
71
+ self.client.on(ToolCallStartEvent, self._once(self._on_tool_call_start))
72
+ self.client.on(ToolCallDoneEvent, self._once(self._on_tool_call_done))
73
+ self.client.on(ServerToolCallEvent, self._once(self._on_server_tool_call))
74
+ self.client.on(SlashCommandStartEvent, self._once(self._on_slash_command_start))
75
+ self.client.on(SlashCommandDoneEvent, self._once(self._on_slash_command_done))
76
+ self.client.on("server_connected", self._on_server_connected)
77
+ self.client.on("server_offline", self._on_server_offline)
78
+ self.client.on("server_subscribed", self._on_server_subscribed)
79
+ self.client.on("turn_error", self._on_turn_error)
80
+ self.client.on("client_event_error", self._on_client_event_error)
81
+ return self
82
+
83
+ def _on_server_connected(self, event):
84
+ self._mark_server_connected(announce=True)
85
+ self.view.refresh_status()
86
+
87
+ def _mark_server_connected(self, announce=False):
88
+ was_offline = not self._server_connected
89
+ self._server_connected = True
90
+ self._server_error = ""
91
+ if announce and was_offline:
92
+ self.view.write_info("server connected")
93
+
94
+ def _on_server_offline(self, event):
95
+ was_connected = self._server_connected
96
+ self._server_connected = False
97
+ self._server_error = event.get("error", "")
98
+ if was_connected:
99
+ self.view.write_info("server offline")
100
+ self.view.refresh_status()
101
+
102
+ def _on_server_subscribed(self, event):
103
+ self._mark_server_connected(announce=event.get("after_sequence") is not None)
104
+ register_tui(base_url=self.client.base_url)
105
+ self.replay_startup_events(event.get("replay_before_sequence"), session_id=event.get("session_id"))
106
+
107
+ def replay_startup_events(self, before_sequence=None, session_id=None):
108
+ try:
109
+ replay = self.client.replay_events(before_sequence=before_sequence, session_id=session_id)
110
+ except Exception as exc:
111
+ self.view.write_error(f"failed to replay session events: {exc}")
112
+ return
113
+ self._replaying_events = True
114
+ self.replay_delta.clear()
115
+ self.view.begin_transcript_batch()
116
+ try:
117
+ for event in self.coalesced_replay_events(replay.get("events") or []):
118
+ self.client.remember_event_sequence(event)
119
+ self.client.emit_local(event)
120
+ finally:
121
+ self._flush_replay_delta_buffer()
122
+ self._replaying_events = False
123
+ self.view.end_transcript_batch()
124
+ self.view.refresh_status()
125
+
126
+ def coalesced_replay_events(self, events):
127
+ pending_delta = ""
128
+ pending_event = None
129
+
130
+ def flush_pending():
131
+ nonlocal pending_delta, pending_event
132
+ if pending_event is None:
133
+ return None
134
+ event = pending_event.copy()
135
+ event.delta = pending_delta
136
+ pending_delta = ""
137
+ pending_event = None
138
+ return event
139
+
140
+ for event in events:
141
+ event = modict(event)
142
+ if event.get("type") == "response_content_delta_event":
143
+ pending_delta += str(event.get("delta") or "")
144
+ pending_event = event
145
+ continue
146
+ flushed = flush_pending()
147
+ if flushed is not None:
148
+ yield flushed
149
+ yield event
150
+ flushed = flush_pending()
151
+ if flushed is not None:
152
+ yield flushed
153
+
154
+ def _on_compaction_completed(self, event):
155
+ self._mark_server_connected()
156
+ self.view.refresh_status()
157
+
158
+ def run(self):
159
+ mouse_enabled = tui_mouse_enabled()
160
+ mouse_mode_hint = "mouse scroll on · drag chat text then Ctrl+C to copy"
161
+ AgentTextualApp(self, mouse_mode_hint).run(mouse=mouse_enabled)
162
+
163
+ def begin_turn_rendering(self):
164
+ self.transcript.begin_agent_turn()
165
+ self.deduper.reset_turn_scope()
166
+
167
+ def username(self):
168
+ try:
169
+ return self.client.config.get("username") or "You"
170
+ except Exception:
171
+ return "You"
172
+
173
+ def agent_name(self):
174
+ try:
175
+ return self.client.config.get("name") or "Pandora"
176
+ except Exception:
177
+ return "Pandora"
178
+
179
+ @property
180
+ def replaying_events(self):
181
+ return self._replaying_events
182
+
183
+ def status_renderable(self):
184
+ if not self._server_connected:
185
+ suffix = f" · {self._server_error}" if self._server_error else ""
186
+ return f"server offline · reconnecting{suffix}"
187
+ try:
188
+ return StatusBar.render(self.client.get_status())
189
+ except Exception as exc:
190
+ self._server_error = str(exc)
191
+ return f"status unavailable · {exc}"
192
+
193
+ def _flush_live_delta(self):
194
+ delta = self.live_delta.pop()
195
+ if delta:
196
+ self.view.write_agent_delta(delta)
197
+ return delta
198
+
199
+ def _request_live_delta_flush(self):
200
+ if not self.live_delta.mark_flush_requested():
201
+ return False
202
+ self.view.schedule_agent_delta_flush()
203
+ return True
204
+
205
+ def _once(self, handler):
206
+ def wrapped(event):
207
+ if not self.deduper.remember_event(event):
208
+ return
209
+ return handler(event)
210
+ return wrapped
211
+
212
+ def _on_message_added(self, event):
213
+ self._mark_server_connected()
214
+ message = event.get("message") or {}
215
+ if message.get("type") == "user_message":
216
+ self.view.write_user(str(message.get("content") or ""))
217
+ self.view.refresh_status()
218
+
219
+ def _on_assistant_turn_start(self, event):
220
+ self._mark_server_connected()
221
+ self.view.write_agent_start(self.agent_name())
222
+ self.view.refresh_status()
223
+
224
+ def _on_assistant_step_start(self, event):
225
+ self._mark_server_connected()
226
+ self._flush_live_delta()
227
+ self.transcript.agent_buffer = ""
228
+ self.view.start_agent_step()
229
+
230
+ def _on_assistant_step_end(self, event):
231
+ self._mark_server_connected()
232
+ self._flush_replay_delta_buffer()
233
+ self._flush_live_delta()
234
+ self.view.end_agent_step()
235
+ self.view.refresh_status()
236
+
237
+ def _on_assistant_turn_end(self, event):
238
+ self._mark_server_connected()
239
+ self._flush_replay_delta_buffer()
240
+ result = event.get("result")
241
+ self._flush_live_delta()
242
+ if result not in (None, True, False):
243
+ self.view.write_info(result)
244
+ self.view.refresh_status()
245
+
246
+ def _on_assistant_turn_error(self, event):
247
+ self._mark_server_connected()
248
+ self._flush_live_delta()
249
+ self.view.write_error(event.get("error", "turn error"))
250
+ self.view.refresh_status()
251
+
252
+ def _on_content_delta(self, event):
253
+ self._mark_server_connected()
254
+ delta = event.get("delta", "")
255
+ if self._replaying_events:
256
+ self.replay_delta.push(delta)
257
+ return
258
+ if self.live_delta.push(delta):
259
+ self._request_live_delta_flush()
260
+
261
+ def _on_response_done(self, event):
262
+ self._mark_server_connected()
263
+ self._flush_replay_delta_buffer()
264
+ self._flush_live_delta()
265
+
266
+ def _flush_replay_delta_buffer(self):
267
+ delta = self.replay_delta.pop()
268
+ if delta:
269
+ self.view.write_agent_delta(delta)
270
+
271
+ def _on_tool_call_start(self, event):
272
+ self._mark_server_connected()
273
+ self._flush_live_delta()
274
+ tool_call = event.get("tool_call") or {}
275
+ name = tool_call.get("name") or "unknown"
276
+ call_id = tool_call.get("call_id") or name
277
+ self._print_tool_call(name, call_id, status="running")
278
+
279
+ def _on_tool_call_done(self, event):
280
+ self._mark_server_connected()
281
+ self._flush_live_delta()
282
+ tool_call = event.get("tool_call") or {}
283
+ name = tool_call.get("name") or "unknown"
284
+ call_id = tool_call.get("call_id") or name
285
+ self._print_tool_call(name, call_id, status="done")
286
+
287
+ def _on_server_tool_call(self, event):
288
+ self._mark_server_connected()
289
+ self._flush_live_delta()
290
+ name = event.get("name") or "unknown"
291
+ call_id = event.get("call_id") or name
292
+ status = event.get("status") or ""
293
+ self._print_tool_call(name, call_id, status=status)
294
+
295
+ def _print_tool_call(self, name, call_id, status=None):
296
+ status = status or "running"
297
+ if not self.deduper.remember_tool(call_id, status):
298
+ return
299
+ self.view.write_tool(name, call_id, status)
300
+
301
+ def _on_slash_command_start(self, event):
302
+ self._mark_server_connected()
303
+ self._flush_live_delta()
304
+ self._print_slash_command(event.get("name") or "", event.get("args") or "", status="running")
305
+
306
+ def _on_slash_command_done(self, event):
307
+ self._mark_server_connected()
308
+ self._flush_live_delta()
309
+ status = event.get("status") or ("failed" if event.get("error") else "done")
310
+ self._print_slash_command(event.get("name") or "", event.get("args") or "", status=status)
311
+ result = "" if event.get("result") is None else str(event.get("result"))
312
+ if result:
313
+ self.view.write_agent_delta(result)
314
+ if event.get("error"):
315
+ self.view.write_error(event.get("error"))
316
+ self.view.refresh_status()
317
+
318
+ def _print_slash_command(self, name, args="", status=None):
319
+ status = status or "running"
320
+ if not self.deduper.remember_slash_command(name, args, status):
321
+ return
322
+ self.view.write_slash_command(name, args, status)
323
+
324
+ def _on_turn_error(self, event):
325
+ self.view.write_error(event.get("error", "turn error"))
326
+ self.view.refresh_status()
327
+
328
+ def _on_client_event_error(self, event):
329
+ message = event.get("detail") or event.get("error", "event stream error")
330
+ if event.get("status_code") == 409:
331
+ print(f"warning: {message}", file=sys.stderr)
332
+ if self.app is not None:
333
+ self.view.exit()
334
+ return
335
+ if event.get("source") == "event_stream":
336
+ was_connected = self._server_connected
337
+ self._server_connected = False
338
+ self._server_error = message
339
+ if was_connected:
340
+ self.view.write_info("server offline")
341
+ self.view.refresh_status()
342
+ return
343
+ self.view.write_error(message)