Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,13 +453,15 @@ Check distro name with `wsl -l -v` in PowerShell.

If `ccb`, `cask`, `cping` commands are not found after running `./install.sh install`:

**Cause:** The install directory (`~/.local/bin`) is not in your PATH.
**Cause:** The install directory (`$CODEX_BIN_DIR` or `$CCB_BIN_DIR`, default `~/.local/bin`) is not in your PATH.

**Note:** Runtime helpers also honor `CCB_BIN_DIR` as an alias for `CODEX_BIN_DIR`.

**Solution:**

```bash
# 1. Check if install directory exists
ls -la ~/.local/bin/
ls -la "${CODEX_BIN_DIR:-${CCB_BIN_DIR:-$HOME/.local/bin}}/"

# 2. Check if PATH includes the directory
echo $PATH | tr ':' '\n' | grep local
Expand All @@ -468,7 +470,7 @@ echo $PATH | tr ':' '\n' | grep local
cat ~/.zshrc | grep local

# 4. If not configured, add manually
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
echo 'export PATH="${CODEX_BIN_DIR:-${CCB_BIN_DIR:-$HOME/.local/bin}}:$PATH"' >> ~/.zshrc

# 5. Reload config
source ~/.zshrc
Expand All @@ -482,7 +484,7 @@ If WezTerm cannot find ccb commands but regular Terminal can:
- Add PATH to `~/.zprofile` as well:

```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zprofile
echo 'export PATH="${CODEX_BIN_DIR:-${CCB_BIN_DIR:-$HOME/.local/bin}}:$PATH"' >> ~/.zprofile
```

Then restart WezTerm completely (Cmd+Q, reopen).
Expand Down
10 changes: 6 additions & 4 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,13 +474,15 @@ cping

如果运行 `./install.sh install` 后找不到 `ccb`、`cask`、`cping` 等命令:

**原因:** 安装目录 (`~/.local/bin`) 不在 PATH 中。
**原因:** 安装目录(`$CODEX_BIN_DIR` 或 `$CCB_BIN_DIR`,默认 `~/.local/bin`)不在 PATH 中。

**提示:** 运行时会将 `CCB_BIN_DIR` 视为 `CODEX_BIN_DIR` 的别名。

**解决方法:**

```bash
# 1. 检查安装目录是否存在
ls -la ~/.local/bin/
ls -la "${CODEX_BIN_DIR:-${CCB_BIN_DIR:-$HOME/.local/bin}}/"

# 2. 检查 PATH 是否包含该目录
echo $PATH | tr ':' '\n' | grep local
Expand All @@ -489,7 +491,7 @@ echo $PATH | tr ':' '\n' | grep local
cat ~/.zshrc | grep local

# 4. 如果没有配置,手动添加
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
echo 'export PATH="${CODEX_BIN_DIR:-${CCB_BIN_DIR:-$HOME/.local/bin}}:$PATH"' >> ~/.zshrc

# 5. 重新加载配置
source ~/.zshrc
Expand All @@ -503,7 +505,7 @@ source ~/.zshrc
- 同时添加 PATH 到 `~/.zprofile`:

```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zprofile
echo 'export PATH="${CODEX_BIN_DIR:-${CCB_BIN_DIR:-$HOME/.local/bin}}:$PATH"' >> ~/.zprofile
```

然后完全重启 WezTerm(Cmd+Q 退出后重新打开)。
Expand Down
117 changes: 73 additions & 44 deletions bin/ccb-completion-hook
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import argparse
import json
import os
import subprocess
import shutil
import sys
from pathlib import Path

Expand Down Expand Up @@ -54,6 +55,43 @@ def env_float(name: str, default: float) -> float:
return default


def _iter_env_bin_dirs() -> list[Path]:
bin_dirs: list[Path] = []
for name in ("CODEX_BIN_DIR", "CCB_BIN_DIR"):
value = (os.environ.get(name) or "").strip()
if value:
bin_dirs.append(Path(value).expanduser())
return bin_dirs


def _build_ask_notify_cmd(ask_cmd: str, caller: str, message: str) -> list[str]:
args = [caller, "--notify", "--no-wrap", message]
if os.name != "nt":
return [ask_cmd, *args]

suffix = Path(ask_cmd).suffix.lower()
if suffix in (".cmd", ".bat"):
return ["cmd.exe", "/c", ask_cmd, *args]
if suffix == ".ps1":
return ["powershell", "-ExecutionPolicy", "Bypass", "-File", ask_cmd, *args]
if suffix in ("", ".py"):
return [sys.executable, ask_cmd, *args]
return [ask_cmd, *args]


def _run_ask_notify(ask_cmd: str, caller: str, message: str, timeout: float) -> int:
try:
result = subprocess.run(
_build_ask_notify_cmd(ask_cmd, caller, message),
capture_output=True,
text=True,
timeout=timeout,
)
return result.returncode
except Exception:
return 0


def load_session_file(session_path: Path) -> dict:
"""Load session file with utf-8-sig encoding (handles BOM from PowerShell)."""
try:
Expand Down Expand Up @@ -187,15 +225,32 @@ def send_via_tmux(pane_id: str, message: str) -> bool:
def find_ask_command() -> str | None:
"""Find the ask command in common locations."""
ask_paths = [
# Prefer the sibling command in the same installed bin folder.
Path(__file__).resolve().parent / "ask",
Path.home() / ".local" / "share" / "codex-dual" / "bin" / "ask",
Path.home() / ".local" / "bin" / "ask",
]
# On Windows, also check LOCALAPPDATA

for bin_dir in _iter_env_bin_dirs():
ask_paths.append(bin_dir / "ask")

install_prefix = (os.environ.get("CODEX_INSTALL_PREFIX") or "").strip()
if install_prefix:
ask_paths.append(Path(install_prefix).expanduser() / "bin" / "ask")

# Windows default install location
if os.name == "nt":
localappdata = os.environ.get("LOCALAPPDATA", "")
if localappdata:
ask_paths.insert(0, Path(localappdata) / "codex-dual" / "bin" / "ask")
ask_paths.append(Path(localappdata) / "codex-dual" / "bin" / "ask")

# Legacy/default locations
ask_paths.extend([
Path.home() / ".local" / "share" / "codex-dual" / "bin" / "ask",
Path.home() / ".local" / "bin" / "ask",
])

found = shutil.which("ask")
if found:
ask_paths.append(Path(found))

for p in ask_paths:
if p.exists():
Expand Down Expand Up @@ -365,7 +420,10 @@ Result: {reply_content}
session_filename = session_files.get(caller, ".claude-session")

# Search for session file in multiple locations (order matters - most specific first)
work_dir = os.environ.get("CCB_WORK_DIR", "")
work_dir = os.environ.get("CCB_WORK_DIR", "").strip()
if not work_dir:
work_dir = os.getcwd()

search_paths = []

# 1. Request's work_dir (most specific)
Expand All @@ -374,13 +432,18 @@ Result: {reply_content}

# 2. Current working directory (fallback)
cwd = os.getcwd()
if cwd != work_dir:
if cwd and cwd != work_dir:
search_paths.append(Path(cwd) / ".ccb_config" / session_filename)

# 3. User's home-based locations
# 3. Explicit install prefix (optional)
install_prefix = (os.environ.get("CODEX_INSTALL_PREFIX") or "").strip()
if install_prefix:
search_paths.append(Path(install_prefix).expanduser() / ".ccb_config" / session_filename)

# 4. Legacy/default install location
search_paths.append(Path.home() / ".local" / "share" / "codex-dual" / ".ccb_config" / session_filename)

# 4. On Windows, also check LOCALAPPDATA
# 5. On Windows, also check LOCALAPPDATA
if os.name == "nt":
localappdata = os.environ.get("LOCALAPPDATA", "")
if localappdata:
Expand Down Expand Up @@ -412,25 +475,7 @@ Result: {reply_content}
ask_cmd = find_ask_command()
if not ask_cmd:
return 0
try:
# Use sys.executable to run ask command on Windows
if os.name == "nt":
result = subprocess.run(
[sys.executable, ask_cmd, caller, "--notify", "--no-wrap", message],
capture_output=True,
text=True,
timeout=timeout
)
else:
result = subprocess.run(
[ask_cmd, caller, "--notify", "--no-wrap", message],
capture_output=True,
text=True,
timeout=timeout
)
return result.returncode
except Exception:
return 0
return _run_ask_notify(ask_cmd, caller, message, timeout)

# Send directly via terminal backend
if send_via_terminal(pane_id, message, terminal, session_data):
Expand All @@ -441,23 +486,7 @@ Result: {reply_content}
ask_cmd = find_ask_command()
if not ask_cmd:
return 0
try:
if os.name == "nt":
subprocess.run(
[sys.executable, ask_cmd, caller, "--notify", "--no-wrap", message],
capture_output=True,
text=True,
timeout=timeout
)
else:
subprocess.run(
[ask_cmd, caller, "--notify", "--no-wrap", message],
capture_output=True,
text=True,
timeout=timeout
)
except Exception:
pass
_run_ask_notify(ask_cmd, caller, message, timeout)
return 0


Expand Down
64 changes: 59 additions & 5 deletions ccb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,55 @@ _MNT_DRIVE_RE = re.compile(r"^/mnt/([A-Za-z])/(.*)$")
_MSYS_DRIVE_RE = re.compile(r"^/([A-Za-z])/(.*)$")


def _get_bin_dir() -> Path:
"""Best-effort resolve the bin dir where CCB helper scripts live.

Priority:
1) CODEX_BIN_DIR / CCB_BIN_DIR env
2) directory of the invoked executable on PATH (shutil.which)
3) default ~/.local/bin
"""
env_bin = (os.environ.get("CODEX_BIN_DIR") or os.environ.get("CCB_BIN_DIR") or "").strip()
if env_bin:
return Path(env_bin).expanduser()

argv0 = (sys.argv[0] or "").strip()
candidates: list[Path] = []
if argv0:
# If invoked via an explicit path, keep its directory (do NOT resolve symlinks).
if ("/" in argv0) or ("\\" in argv0):
candidates.append(Path(argv0).expanduser())
found = shutil.which(argv0)
if found:
candidates.append(Path(found))

found_ccb = shutil.which("ccb")
if found_ccb:
candidates.append(Path(found_ccb))

for p in candidates:
try:
if p.exists():
return p.parent
except Exception:
continue

return Path.home() / ".local" / "bin"


def _find_helper_script(name: str) -> Path | None:
"""Locate an installed helper script by name."""
found = shutil.which(name)
if found:
return Path(found)

for base in (_get_bin_dir(), script_dir / "config"):
p = base / name
if p.exists():
return p
return None


def _looks_like_windows_path(value: str) -> bool:
s = value.strip()
if not s:
Expand Down Expand Up @@ -901,14 +950,15 @@ class AILauncher:
Enable/disable CCB tmux UI theming for the *current tmux session*.

This is session-scoped and reversible (saves/restores user options) via helper scripts
installed to `~/.local/bin/`.
installed to BIN_DIR (see env `CODEX_BIN_DIR`).
"""
if self.terminal_type != "tmux":
return
if not os.environ.get("TMUX"):
return
script = Path.home() / ".local" / "bin" / ("ccb-tmux-on.sh" if active else "ccb-tmux-off.sh")
if not script.exists():
script_name = "ccb-tmux-on.sh" if active else "ccb-tmux-off.sh"
script = _find_helper_script(script_name)
if not script:
return
try:
debug = os.environ.get("CCB_DEBUG") in ("1", "true", "yes")
Expand Down Expand Up @@ -3847,9 +3897,13 @@ def _detect_cca() -> tuple[str | None, str | None]:
install_dir = _infer_install_dir_from_exe(exe)
return str(exe.resolve()), str(install_dir)
candidates = [
Path.home() / ".local/share/claude_code_autoflow",
Path.home() / ".local/bin/cca",
Path.home() / ".local" / "share" / "claude_code_autoflow",
]
# If user configured a custom bin dir for tools, also consider it for legacy fallbacks.
env_bin_dir = (os.environ.get("CODEX_BIN_DIR") or os.environ.get("CCB_BIN_DIR") or "").strip()
if env_bin_dir:
candidates.append(Path(env_bin_dir).expanduser() / "cca")
candidates.append(Path.home() / ".local" / "bin" / "cca")
# Windows 特定路径
if platform.system() == "Windows":
localappdata = os.environ.get("LOCALAPPDATA", "")
Expand Down
8 changes: 6 additions & 2 deletions config/tmux-ccb.conf
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ bind M set -g mouse off \; display "Mouse OFF"

bind r source-file ~/.tmux.conf \; display "Config reloaded!"

# Manually toggle CCB theme for current session (optional).
bind-key C run-shell "#{@ccb_bin_dir}/ccb-tmux-on.sh"
bind-key V run-shell "#{@ccb_bin_dir}/ccb-tmux-off.sh"

# -----------------------------------------------------------------------------
# Session Management
# -----------------------------------------------------------------------------
Expand All @@ -191,9 +195,9 @@ set -g visual-activity off
# NOTE:
# CCB intentionally does not set any persistent statusbar/theme options here.
# The CCB theme is applied per-session only while CCB is active via:
# - `~/.local/bin/ccb-tmux-on.sh`
# - `$CODEX_BIN_DIR/ccb-tmux-on.sh` (or default `~/.local/bin/ccb-tmux-on.sh`)
# and restored on exit via:
# - `~/.local/bin/ccb-tmux-off.sh`
# - `$CODEX_BIN_DIR/ccb-tmux-off.sh` (or default `~/.local/bin/ccb-tmux-off.sh`)
#
# This avoids clobbering your existing tmux theme when CCB is not running.

Expand Down
Loading
Loading