From 6e7b1b6b5efd8ada9f6a0184ce6650ba3c9187a3 Mon Sep 17 00:00:00 2001 From: Shawn Pana Date: Sat, 23 May 2026 14:41:39 -0700 Subject: [PATCH] Launch Codex terminal sessions --- agent/box_agent.py | 12 +++++++++++- agent/test_box_agent.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 agent/test_box_agent.py diff --git a/agent/box_agent.py b/agent/box_agent.py index 29b7260..9e0993e 100644 --- a/agent/box_agent.py +++ b/agent/box_agent.py @@ -319,7 +319,7 @@ def start( `window_id` is the tmux session name. The cloud picks it (default `bux-w1`); we sanitize defensively. First attach to a window creates it via `tmux new-session -A -d` and seeds the launch - command (claude / bash). Subsequent attaches just open another + command (claude / codex / bash). Subsequent attaches just open another client onto the same tmux session. `launch` and `dsp_enabled` are only honored when we're CREATING @@ -411,6 +411,7 @@ def _ensure_tmux_window( session. Splitting create-vs-attach this way avoids "session not found" races and lets us seed `launch` only on first create. """ + import shlex import subprocess def _run(args: list[str]) -> int: @@ -442,6 +443,10 @@ def _run(args: list[str]) -> int: # `; exec bash -l` so when claude quits the user lands in a # bash prompt instead of the tmux session ending. cmd_str = f'{claude_cmd}; exec bash -l' + elif launch == 'codex': + # Same tmux lifecycle as Claude: Codex owns the first command, + # and quitting drops the user to a normal login shell. + cmd_str = f'{shlex.quote(CODEX_BIN)}; exec bash -l' else: cmd_str = 'exec bash -l' @@ -884,6 +889,11 @@ async def _handle(self, raw: str | bytes) -> None: if new != self._dsp_enabled: LOG.info('dsp_enabled %s → %s', self._dsp_enabled, new) self._dsp_enabled = new + elif cmd == 'update_default_agent': + # Cloud owns the persisted default. The launch choice is carried + # on each shell_attach, so the box-agent only ACKs this command to + # avoid warning noise on older/no-op setting pushes. + await self._send({'type': 'ack', 'cmd': cmd, 'ok': True}) elif cmd == 'shell_input': import base64 as _b64 diff --git a/agent/test_box_agent.py b/agent/test_box_agent.py new file mode 100644 index 0000000..2e93e9a --- /dev/null +++ b/agent/test_box_agent.py @@ -0,0 +1,36 @@ +import unittest +import sys +import types +from unittest import mock + +sys.modules.setdefault('websockets', types.ModuleType('websockets')) + +from agent import box_agent + + +class ShellSessionLaunchTest(unittest.TestCase): + def test_codex_launch_seeds_tmux_window_with_codex_cli(self) -> None: + calls: list[list[str]] = [] + + def fake_run(args: list[str], **_kwargs: object) -> mock.Mock: + calls.append(args) + # First call is has-session: report missing so creation path runs. + return mock.Mock(returncode=1 if len(calls) == 1 else 0) + + with ( + mock.patch.object(box_agent, 'CODEX_BIN', '/usr/local/bin/codex'), + mock.patch('subprocess.run', side_effect=fake_run), + ): + box_agent.ShellSession._ensure_tmux_window( + 'bux-w1', + launch='codex', + dsp_enabled=True, + ) + + self.assertEqual(calls[0], ['/usr/bin/tmux', 'has-session', '-t', 'bux-w1']) + self.assertEqual(calls[1][-3:], ['/bin/bash', '-lc', '/usr/local/bin/codex; exec bash -l']) + self.assertEqual(calls[2], ['/usr/bin/tmux', 'set-window-option', '-t', 'bux-w1', 'aggressive-resize', 'on']) + + +if __name__ == '__main__': + unittest.main()