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.
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/CHANGELOG.md +13 -0
- {codex_agent_framework-0.1.28/codex_agent_framework.egg-info → codex_agent_framework-0.1.29}/PKG-INFO +1 -1
- codex_agent_framework-0.1.29/codex_agent/tui/app.py +211 -0
- codex_agent_framework-0.1.29/codex_agent/tui/chat.py +343 -0
- codex_agent_framework-0.1.29/codex_agent/tui/log.py +225 -0
- codex_agent_framework-0.1.29/codex_agent/tui/state.py +148 -0
- codex_agent_framework-0.1.29/codex_agent/tui/status_bar.py +86 -0
- codex_agent_framework-0.1.29/codex_agent/tui/style.py +56 -0
- codex_agent_framework-0.1.29/codex_agent/tui/transcript.py +337 -0
- codex_agent_framework-0.1.29/codex_agent/version.py +1 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29/codex_agent_framework.egg-info}/PKG-INFO +1 -1
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/SOURCES.txt +6 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/pyproject.toml +1 -1
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_chat.py +206 -106
- codex_agent_framework-0.1.28/codex_agent/tui/chat.py +0 -1293
- codex_agent_framework-0.1.28/codex_agent/version.py +0 -1
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/LICENSE +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/README.md +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/__main__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/agent.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/agent_runtime.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/ai.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/browser.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_commands.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/system.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/content/vision.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/context/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/files/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/files/tools.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
- {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
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/python/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/realtime/session.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/headless.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/main.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/output.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/root.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/cli/runner.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/client.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/config.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/context.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/desktop.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/event.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/hooks.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/mainloop.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/message.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/plugin.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/prompts/system_prompt.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/runtime.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/scheduler.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/scripts/install-system-dependencies.sh +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/server/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/server/app.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/server/core.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/service.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/sessions.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/status.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/stream.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/stream_utils.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tray.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tui/__init__.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/tui/lifecycle.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/utils.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/entry_points.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/requires.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/dependencies.txt +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/setup.cfg +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_agent.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_agent_runtime.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_ai.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_browser.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_builtin_config.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_cli.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_cli_headless.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_cli_root.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_client.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_get_text_browser.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_hooks.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_local_desktop.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_memory.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_messages.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_planner.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_scheduler.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_server.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_service.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_tray.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_tui.py +0 -0
- {codex_agent_framework-0.1.28 → codex_agent_framework-0.1.29}/tests/test_utils.py +0 -0
- {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.
|
|
@@ -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)
|