codex-statusline 0.2.2__tar.gz → 0.3.1__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 (32) hide show
  1. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/PKG-INFO +9 -7
  2. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/README.md +8 -6
  3. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/PKG-INFO +9 -7
  4. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/SOURCES.txt +2 -0
  5. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/entry_points.txt +1 -0
  6. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/pyproject.toml +2 -1
  7. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/__init__.py +1 -1
  8. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/cli.py +6 -18
  9. codex_statusline-0.3.1/src/doctor.py +111 -0
  10. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/launcher.py +6 -17
  11. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/render.py +2 -1
  12. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/update.py +9 -3
  13. codex_statusline-0.3.1/tests/test_doctor.py +74 -0
  14. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_startup.py +2 -0
  15. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_update.py +2 -0
  16. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/LICENSE +0 -0
  17. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/dependency_links.txt +0 -0
  18. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/codex_statusline.egg-info/top_level.txt +0 -0
  19. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/setup.cfg +0 -0
  20. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/__main__.py +0 -0
  21. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/configure.py +0 -0
  22. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/data.py +0 -0
  23. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/entry.py +0 -0
  24. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/hook.py +0 -0
  25. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/src/manage.py +0 -0
  26. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_auto_shells.py +0 -0
  27. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_configure.py +0 -0
  28. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_data.py +0 -0
  29. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_fish.py +0 -0
  30. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_management.py +0 -0
  31. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_render.py +0 -0
  32. {codex_statusline-0.2.2 → codex_statusline-0.3.1}/tests/test_tmux.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-statusline
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only.
5
5
  Author: Zimo Xu
6
6
  License-Expression: MIT
@@ -59,7 +59,7 @@ Prefer to do it yourself:
59
59
 
60
60
  ```sh
61
61
  uv tool install codex-statusline # or: pipx install codex-statusline
62
- codex-statusline install # add --dry-run to see the plan first
62
+ cxbar install # add --dry-run to see the plan first
63
63
  ```
64
64
 
65
65
  Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
@@ -73,17 +73,19 @@ codex # same as always, now with a status bar
73
73
  codex resume --last
74
74
  codex exec 'task' # non-interactive: passed straight through, no bar
75
75
 
76
- codex-statusline config # pick segments, theme and colors interactively
77
- codex-statusline update # upgrade, whichever way you installed it
78
- codex-statusline doctor # check versions and dependencies
79
- codex-statusline uninstall
76
+ cxbar config # pick segments, theme and colors interactively
77
+ cxbar update # upgrade, whichever way you installed it
78
+ cxbar doctor # check your setup; prints the fix for anything wrong
79
+ cxbar uninstall
80
80
  ```
81
81
 
82
+ `cxbar` is the short name for `codex-statusline`; both work.
83
+
82
84
  Uninstall removes only what this tool added and still matches exactly. If you edited its block, it stops instead of overwriting. Backups live in `~/.local/share/codex-statusline/`.
83
85
 
84
86
  ## Configuration
85
87
 
86
- Run `codex-statusline config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
88
+ Run `cxbar config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
87
89
 
88
90
  ```text
89
91
  [x] ctx context used
@@ -38,7 +38,7 @@ Prefer to do it yourself:
38
38
 
39
39
  ```sh
40
40
  uv tool install codex-statusline # or: pipx install codex-statusline
41
- codex-statusline install # add --dry-run to see the plan first
41
+ cxbar install # add --dry-run to see the plan first
42
42
  ```
43
43
 
44
44
  Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
@@ -52,17 +52,19 @@ codex # same as always, now with a status bar
52
52
  codex resume --last
53
53
  codex exec 'task' # non-interactive: passed straight through, no bar
54
54
 
55
- codex-statusline config # pick segments, theme and colors interactively
56
- codex-statusline update # upgrade, whichever way you installed it
57
- codex-statusline doctor # check versions and dependencies
58
- codex-statusline uninstall
55
+ cxbar config # pick segments, theme and colors interactively
56
+ cxbar update # upgrade, whichever way you installed it
57
+ cxbar doctor # check your setup; prints the fix for anything wrong
58
+ cxbar uninstall
59
59
  ```
60
60
 
61
+ `cxbar` is the short name for `codex-statusline`; both work.
62
+
61
63
  Uninstall removes only what this tool added and still matches exactly. If you edited its block, it stops instead of overwriting. Backups live in `~/.local/share/codex-statusline/`.
62
64
 
63
65
  ## Configuration
64
66
 
65
- Run `codex-statusline config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
67
+ Run `cxbar config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
66
68
 
67
69
  ```text
68
70
  [x] ctx context used
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-statusline
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only.
5
5
  Author: Zimo Xu
6
6
  License-Expression: MIT
@@ -59,7 +59,7 @@ Prefer to do it yourself:
59
59
 
60
60
  ```sh
61
61
  uv tool install codex-statusline # or: pipx install codex-statusline
62
- codex-statusline install # add --dry-run to see the plan first
62
+ cxbar install # add --dry-run to see the plan first
63
63
  ```
64
64
 
65
65
  Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
@@ -73,17 +73,19 @@ codex # same as always, now with a status bar
73
73
  codex resume --last
74
74
  codex exec 'task' # non-interactive: passed straight through, no bar
75
75
 
76
- codex-statusline config # pick segments, theme and colors interactively
77
- codex-statusline update # upgrade, whichever way you installed it
78
- codex-statusline doctor # check versions and dependencies
79
- codex-statusline uninstall
76
+ cxbar config # pick segments, theme and colors interactively
77
+ cxbar update # upgrade, whichever way you installed it
78
+ cxbar doctor # check your setup; prints the fix for anything wrong
79
+ cxbar uninstall
80
80
  ```
81
81
 
82
+ `cxbar` is the short name for `codex-statusline`; both work.
83
+
82
84
  Uninstall removes only what this tool added and still matches exactly. If you edited its block, it stops instead of overwriting. Backups live in `~/.local/share/codex-statusline/`.
83
85
 
84
86
  ## Configuration
85
87
 
86
- Run `codex-statusline config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
88
+ Run `cxbar config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
87
89
 
88
90
  ```text
89
91
  [x] ctx context used
@@ -11,6 +11,7 @@ src/__main__.py
11
11
  src/cli.py
12
12
  src/configure.py
13
13
  src/data.py
14
+ src/doctor.py
14
15
  src/entry.py
15
16
  src/hook.py
16
17
  src/launcher.py
@@ -20,6 +21,7 @@ src/update.py
20
21
  tests/test_auto_shells.py
21
22
  tests/test_configure.py
22
23
  tests/test_data.py
24
+ tests/test_doctor.py
23
25
  tests/test_fish.py
24
26
  tests/test_management.py
25
27
  tests/test_render.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  codex-statusline = src.cli:main
3
+ cxbar = src.cli:main
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codex-statusline"
3
- version = "0.2.2"
3
+ version = "0.3.1"
4
4
  description = "A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only."
5
5
  requires-python = ">=3.11"
6
6
  license = "MIT"
@@ -23,6 +23,7 @@ Changelog = "https://github.com/Moviw/codex-statusline/blob/main/CHANGELOG.md"
23
23
 
24
24
  [project.scripts]
25
25
  codex-statusline = "src.cli:main"
26
+ cxbar = "src.cli:main" # short alias
26
27
 
27
28
  [build-system]
28
29
  requires = ["setuptools>=77"]
@@ -1,3 +1,3 @@
1
1
  """A local-only tmux status area for the official Codex CLI."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.1"
@@ -1,7 +1,4 @@
1
1
  import argparse
2
- import json
3
- import os
4
- import shutil
5
2
  import sys
6
3
  from pathlib import Path
7
4
 
@@ -10,7 +7,7 @@ from . import __version__
10
7
 
11
8
  def main():
12
9
  args = sys.argv[1:]
13
- from .launcher import child, launch, official, raw, version
10
+ from .launcher import child, launch, raw
14
11
 
15
12
  if args and args[0] in ("launch", "raw"):
16
13
  try:
@@ -25,11 +22,11 @@ def main():
25
22
  record()
26
23
  return 0
27
24
  parser = argparse.ArgumentParser(
28
- prog="codex-statusline",
25
+ prog="cxbar" if Path(sys.argv[0]).name == "cxbar" else "codex-statusline",
29
26
  description="A status bar for Codex CLI: context, 5h/weekly quota, and tokens. "
30
27
  "Run `codex` as usual once installed.",
31
28
  )
32
- parser.add_argument("--version", action="version", version=__version__)
29
+ parser.add_argument("-V", "-v", "--version", action="version", version=__version__)
33
30
  sub = parser.add_subparsers(dest="action", required=True, metavar="<command>", title="commands")
34
31
  commands = {
35
32
  "install": "hook the status bar into `codex` for your shells",
@@ -68,18 +65,9 @@ def main():
68
65
 
69
66
  return update()
70
67
  if ns.action == "doctor":
71
- path = official()
72
- data = {
73
- "python": sys.version.split()[0],
74
- "codex": path,
75
- "codex_version": ".".join(map(str, version(path))),
76
- "tmux": shutil.which("tmux"),
77
- "shell": os.environ.get("SHELL"),
78
- "tested_codex": "0.159.0",
79
- "credentials_read": False,
80
- }
81
- print(json.dumps(data, indent=2))
82
- return 0 if data["tmux"] and version(path) >= (0, 159, 0) else 1
68
+ from .doctor import doctor
69
+
70
+ return doctor()
83
71
  except (OSError, ValueError, RuntimeError, KeyError) as error:
84
72
  print(f"codex-statusline: {error}", file=sys.stderr)
85
73
  return 1
@@ -0,0 +1,111 @@
1
+ """`cxbar doctor`: one line per check, and the command that fixes each problem."""
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ import tomllib
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .launcher import official, version
11
+ from .manage import STATUS_MESSAGE, hook_group, paths, text, tilde
12
+ from .update import is_newer, latest_version
13
+
14
+
15
+ def command_name() -> str:
16
+ return "cxbar" if shutil.which("cxbar") else "codex-statusline"
17
+
18
+
19
+ def checks(home: Path, latest=latest_version) -> list[tuple[bool | None, str, str | None]]:
20
+ """(ok, message, fix): ok is True, False, or None for information only."""
21
+ cmd = command_name()
22
+ out = []
23
+ try:
24
+ found = version(official())
25
+ shown = ".".join(map(str, found))
26
+ out.append((found >= (0, 159, 0), f"Codex CLI {shown}", "upgrade Codex CLI to 0.159.0+"))
27
+ except (RuntimeError, OSError, subprocess.TimeoutExpired):
28
+ out.append((False, "Codex CLI not found on PATH", "install Codex CLI"))
29
+ tmux = shutil.which("tmux")
30
+ if tmux:
31
+ tmux_version = subprocess.run([tmux, "-V"], capture_output=True, text=True).stdout.strip()
32
+ out.append((True, tmux_version or "tmux", None))
33
+ else:
34
+ out.append((False, "tmux not found", "brew install tmux / sudo apt install tmux"))
35
+
36
+ codex_home, _, manifest_path = paths(home, "all")
37
+ try:
38
+ manifest = json.loads(text(manifest_path) or "{}")
39
+ except ValueError:
40
+ manifest = {}
41
+ if not manifest:
42
+ out.append((False, "not hooked into your shell yet", f"{cmd} install"))
43
+ return out + [_update_check(cmd, latest)]
44
+
45
+ if manifest.get("hook_group") != hook_group():
46
+ other = manifest["hook_group"]["hooks"][0]["command"].removesuffix(" hook")
47
+ out.append((False, f"codex runs another copy: {other}", f"{cmd} install"))
48
+ else:
49
+ shells = [Path(item["path"]) for item in manifest.get("shells", [])]
50
+ missing = [p for p in shells if item_block(manifest, p) not in text(p)]
51
+ names = ", ".join(tilde(p, home) for p in shells)
52
+ out.append((not missing, f"codex is hooked into {names}", f"{cmd} install"))
53
+
54
+ hooks_path = Path(manifest["hooks_path"])
55
+ try:
56
+ groups = json.loads(text(hooks_path) or "{}").get("hooks", {}).get("SessionStart", [])
57
+ except ValueError:
58
+ groups = []
59
+ index = next((i for i, g in enumerate(groups) if g == manifest["hook_group"]), None)
60
+ if index is None:
61
+ out.append((False, f"hook missing from {tilde(hooks_path, home)}", f"{cmd} install"))
62
+ else:
63
+ try:
64
+ state = (
65
+ tomllib.loads(text(codex_home / "config.toml")).get("hooks", {}).get("state", {})
66
+ )
67
+ except tomllib.TOMLDecodeError:
68
+ state = {}
69
+ # Codex keys trust by the hooks.json path as it sees it; compare resolved paths so a
70
+ # symlinked ~/.codex (or macOS /var -> /private/var) still matches.
71
+ suffix = f":session_start:{index}:0"
72
+ trusted = any(
73
+ key.endswith(suffix)
74
+ and Path(key.removesuffix(suffix)).resolve() == hooks_path.resolve()
75
+ and "trusted_hash" in value
76
+ for key, value in state.items()
77
+ if isinstance(value, dict)
78
+ )
79
+ out.append(
80
+ (True, "hook approved in Codex", None)
81
+ if trusted
82
+ else (
83
+ False,
84
+ "hook not approved in Codex yet",
85
+ f"start codex, approve '{STATUS_MESSAGE}'",
86
+ )
87
+ )
88
+ return out + [_update_check(cmd, latest)]
89
+
90
+
91
+ def item_block(manifest: dict, path: Path) -> str:
92
+ return next(i["block"] for i in manifest["shells"] if Path(i["path"]) == path)
93
+
94
+
95
+ def _update_check(cmd: str, latest) -> tuple[bool | None, str, str | None]:
96
+ found = latest()
97
+ if found is None:
98
+ return (None, "could not check for updates (offline?)", None)
99
+ if is_newer(found):
100
+ return (False, f"{found} available", f"{cmd} update")
101
+ return (True, "latest version", None)
102
+
103
+
104
+ def doctor(home: Path | None = None) -> int:
105
+ results = checks(home or Path.home())
106
+ print(f"codex-statusline {__version__}")
107
+ for ok, message, fix in results:
108
+ print(f" {'✓' if ok else '✗' if ok is False else '·'} {message}")
109
+ if ok is False and fix:
110
+ print(f" → {fix}")
111
+ return 1 if any(ok is False for ok, _, _ in results) else 0
@@ -143,19 +143,6 @@ def binding_for(bindings, hint, launch_id):
143
143
  return candidates[0] if len(candidates) == 1 else None
144
144
 
145
145
 
146
- def branch(cwd):
147
- try:
148
- p = subprocess.run(
149
- ["git", "-C", cwd, "symbolic-ref", "--quiet", "--short", "HEAD"],
150
- capture_output=True,
151
- text=True,
152
- timeout=0.7,
153
- )
154
- return p.stdout.strip() if p.returncode == 0 else None
155
- except (OSError, subprocess.TimeoutExpired):
156
- return None
157
-
158
-
159
146
  def placeholder(width=80):
160
147
  """Use the normal layout before telemetry arrives or while it is unavailable."""
161
148
  from .data import newest_quotas, sessions_dir
@@ -216,6 +203,10 @@ def launch(args):
216
203
  "set -g allow-rename off\n"
217
204
  "set -g set-titles off\n"
218
205
  "set -g mouse on\n"
206
+ # Every key belongs to Codex: no prefix (Ctrl-B), no wait after Esc.
207
+ "set -g prefix None\n"
208
+ "unbind C-b\n"
209
+ "set -s escape-time 10\n"
219
210
  "set -g history-limit 10000\n"
220
211
  "set -g remain-on-exit on\n"
221
212
  "set -g exit-empty on\n"
@@ -247,6 +238,7 @@ def launch(args):
247
238
  cfg = load_config()
248
239
  reader = LogReader()
249
240
  latest = {}
241
+ updater = "cxbar" if shutil.which("cxbar") else "codex-statusline"
250
242
  if cfg["update_check"]:
251
243
  from .update import newer_version
252
244
 
@@ -254,7 +246,6 @@ def launch(args):
254
246
  target=lambda: latest.update(version=newer_version()), daemon=True
255
247
  ).start()
256
248
  sessions, readers, others, scan_at = sessions_dir(), {}, {}, 0.0
257
- last_branch, branch_at = None, 0
258
249
  last_line = None
259
250
  while not stop.is_set():
260
251
  try:
@@ -290,9 +281,7 @@ def launch(args):
290
281
  )
291
282
  state["quotas"] = merge_quotas(state["quotas"], others)
292
283
  state["update"] = latest.get("version")
293
- if time.monotonic() - branch_at > 3:
294
- last_branch, branch_at = branch(state["cwd"]), time.monotonic()
295
- state["branch"] = last_branch
284
+ state["updater"] = updater
296
285
  line = render(
297
286
  state,
298
287
  int(width or 80),
@@ -278,7 +278,8 @@ def render(
278
278
  hint = None
279
279
  if isinstance(update, str) and re.fullmatch(r"\d+(\.\d+)*", update):
280
280
  arrow = "^" if ascii_only else "↑"
281
- hint = (f"{arrow} update available: codex-statusline update", f"{arrow} update", "warning")
281
+ tool = "cxbar" if state.get("updater") == "cxbar" else "codex-statusline"
282
+ hint = (f"{arrow} update available: {tool} update", f"{arrow} update", "warning")
282
283
  head, rest = parts[0], parts[1:]
283
284
  compact_head = (head[1], head[2])
284
285
  # Widest first: all detail, compact head, all compact, then drop segments.
@@ -24,8 +24,8 @@ def is_newer(latest: str, current: str = __version__) -> bool:
24
24
  return bool(_parse(latest)) and _parse(latest) > _parse(current)
25
25
 
26
26
 
27
- def newer_version(timeout: float = 2) -> str | None:
28
- """Latest release if newer than this one; None on any failure. Sends only a plain GET."""
27
+ def latest_version(timeout: float = 2) -> str | None:
28
+ """Latest release on PyPI; None on any failure. Sends only a plain GET."""
29
29
  try:
30
30
  request = urllib.request.Request(
31
31
  PYPI, headers={"User-Agent": f"codex-statusline/{__version__}"}
@@ -34,7 +34,13 @@ def newer_version(timeout: float = 2) -> str | None:
34
34
  latest = json.load(response)["info"]["version"]
35
35
  except Exception:
36
36
  return None
37
- return latest if isinstance(latest, str) and is_newer(latest) else None
37
+ return latest if isinstance(latest, str) else None
38
+
39
+
40
+ def newer_version(timeout: float = 2) -> str | None:
41
+ """Latest release if newer than this one; None otherwise or on any failure."""
42
+ latest = latest_version(timeout)
43
+ return latest if latest and is_newer(latest) else None
38
44
 
39
45
 
40
46
  def upgrade_command(prefix: str = sys.prefix, package: Path = PACKAGE) -> list[str]:
@@ -0,0 +1,74 @@
1
+ import argparse
2
+ import contextlib
3
+ import io
4
+ import json
5
+ import os
6
+ import tempfile
7
+ import unittest
8
+ from pathlib import Path
9
+ from unittest.mock import patch
10
+
11
+ from src import doctor, manage
12
+
13
+
14
+ class DoctorTests(unittest.TestCase):
15
+ def setUp(self):
16
+ self.temp = tempfile.TemporaryDirectory()
17
+ # A symlinked home, like macOS /var -> /private/var or a linked ~/.codex.
18
+ real = Path(self.temp.name) / "real"
19
+ real.mkdir()
20
+ self.home = Path(self.temp.name) / "link"
21
+ self.home.symlink_to(real)
22
+ env = {"CODEX_HOME": str(self.home / ".codex"), "ZDOTDIR": str(self.home)}
23
+ for p in (
24
+ patch.dict(os.environ, env),
25
+ patch.object(doctor, "official", return_value="/bin/codex"),
26
+ patch.object(doctor, "version", return_value=(0, 159, 0)),
27
+ ):
28
+ p.start()
29
+ self.addCleanup(p.stop)
30
+ self.addCleanup(self.temp.cleanup)
31
+
32
+ def run_checks(self, latest="0.0.1"):
33
+ return {msg: (ok, fix) for ok, msg, fix in doctor.checks(self.home, lambda: latest)}
34
+
35
+ def install(self):
36
+ args = argparse.Namespace(home=str(self.home), shell="zsh", yes=True, dry_run=False)
37
+ with contextlib.redirect_stdout(io.StringIO()):
38
+ manage.installation(args)
39
+
40
+ def test_not_installed_points_to_install(self):
41
+ results = self.run_checks()
42
+ ok, fix = results["not hooked into your shell yet"]
43
+ self.assertFalse(ok)
44
+ self.assertTrue(fix.endswith(" install"))
45
+
46
+ def test_installed_hook_waits_for_approval_then_passes(self):
47
+ self.install()
48
+ self.assertFalse(self.run_checks()["hook not approved in Codex yet"][0])
49
+ hooks = self.home / ".codex" / "hooks.json"
50
+ key = f"{hooks}:session_start:0:0"
51
+ (self.home / ".codex" / "config.toml").write_text(
52
+ f'[hooks.state."{key}"]\ntrusted_hash = "sha256:x"\n'
53
+ )
54
+ results = self.run_checks()
55
+ self.assertTrue(results["hook approved in Codex"][0])
56
+ self.assertTrue(results["codex is hooked into ~/.zshrc"][0])
57
+ self.assertTrue(results["latest version"][0])
58
+
59
+ def test_stale_copy_and_update_are_flagged(self):
60
+ self.install()
61
+ with patch.object(doctor, "hook_group", return_value={"hooks": [{"command": "new"}]}):
62
+ results = self.run_checks(latest="99.0.0")
63
+ stale = [m for m in results if m.startswith("codex runs another copy")]
64
+ self.assertEqual(len(stale), 1)
65
+ self.assertEqual(results["99.0.0 available"][1].split()[-1], "update")
66
+ self.assertIsNone(doctor._update_check("cxbar", lambda: None)[0])
67
+
68
+ def test_doctor_exit_code(self):
69
+ with contextlib.redirect_stdout(io.StringIO()) as out:
70
+ with patch.object(doctor, "latest_version", return_value=None):
71
+ code = doctor.doctor(self.home)
72
+ self.assertEqual(code, 1)
73
+ self.assertIn("✗ not hooked into your shell yet", out.getvalue())
74
+ json.dumps(out.getvalue()) # plain text, printable
@@ -118,6 +118,8 @@ class StartupTests(unittest.TestCase):
118
118
  self.assertIn("CTX USED", conf)
119
119
  # Codex asks tmux for #{mouse}; off makes the wheel recall old prompts.
120
120
  self.assertIn("set -g mouse on", conf)
121
+ self.assertIn("set -g prefix None", conf)
122
+ self.assertIn("set -s escape-time 10", conf)
121
123
  self.assertNotIn("binding pending", conf)
122
124
  # error.json is written before the fallback status is applied.
123
125
  # Faster Linux startup can observe that intermediate state.
@@ -43,6 +43,8 @@ class UpdateTests(unittest.TestCase):
43
43
  state = {"context_used": 10, "now": 1000, "update": "0.2.0", "quotas": {}}
44
44
  wide = render(state, 240, tmux=False)
45
45
  self.assertIn("↑ update available: codex-statusline update", wide)
46
+ short = render(dict(state, updater="cxbar"), 240, tmux=False)
47
+ self.assertIn("↑ update available: cxbar update", short)
46
48
  self.assertIn("^ update available", render(state, 240, tmux=False, ascii_only=True))
47
49
  self.assertNotIn("update", render(state, 40, tmux=False))
48
50
  self.assertNotIn("update", render(dict(state, update="#(evil)"), 240, tmux=False))