Two reliability fixes for response injection:
1. **Zellij binary resolution** (context.py, state.py, control.py)
When AMC is started via macOS launchctl, PATH is minimal and may not
include Homebrew's bin directory. The new `_resolve_zellij_bin()`
function tries `shutil.which("zellij")` first, then falls back to
common installation paths:
- /opt/homebrew/bin/zellij (Apple Silicon Homebrew)
- /usr/local/bin/zellij (Intel Homebrew)
- /usr/bin/zellij
All subprocess calls now use ZELLIJ_BIN instead of hardcoded "zellij".
2. **Two-step Enter injection** (control.py)
Previously, text and Enter were sent together, causing race conditions
where Claude Code would receive only the Enter key (blank submit).
Now uses `_inject_text_then_enter()`:
- Send text (without Enter)
- Wait for configurable delay (default 200ms)
- Send Enter separately
Delay is configurable via AMC_SUBMIT_ENTER_DELAY_MS env var (0-2000ms).
3. **Documentation updates** (README.md)
- Update file table: dashboard-preact.html → dashboard/
- Clarify plugin is required (not optional) for pane-targeted injection
- Document AMC_ALLOW_UNSAFE_WRITE_CHARS_FALLBACK env var
- Note about Zellij resolution for launchctl compatibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
import shutil
|
|
from pathlib import Path
|
|
import threading
|
|
|
|
# Claude Code conversation directory
|
|
CLAUDE_PROJECTS_DIR = Path.home() / ".claude" / "projects"
|
|
|
|
# Codex conversation directory
|
|
CODEX_SESSIONS_DIR = Path.home() / ".codex" / "sessions"
|
|
|
|
# Plugin path for zellij-send-keys
|
|
ZELLIJ_PLUGIN = Path.home() / ".config" / "zellij" / "plugins" / "zellij-send-keys.wasm"
|
|
|
|
|
|
def _resolve_zellij_bin():
|
|
"""Resolve zellij binary even when PATH is minimal (eg launchctl)."""
|
|
from_path = shutil.which("zellij")
|
|
if from_path:
|
|
return from_path
|
|
|
|
common_paths = (
|
|
"/opt/homebrew/bin/zellij", # Apple Silicon Homebrew
|
|
"/usr/local/bin/zellij", # Intel Homebrew
|
|
"/usr/bin/zellij",
|
|
)
|
|
for candidate in common_paths:
|
|
p = Path(candidate)
|
|
if p.exists() and p.is_file():
|
|
return str(p)
|
|
return "zellij" # Fallback for explicit error reporting by subprocess
|
|
|
|
|
|
ZELLIJ_BIN = _resolve_zellij_bin()
|
|
|
|
# Runtime data lives in XDG data dir
|
|
DATA_DIR = Path.home() / ".local" / "share" / "amc"
|
|
SESSIONS_DIR = DATA_DIR / "sessions"
|
|
EVENTS_DIR = DATA_DIR / "events"
|
|
|
|
# Source files live in project directory (relative to this module)
|
|
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
|
DASHBOARD_DIR = PROJECT_DIR / "dashboard"
|
|
|
|
PORT = 7400
|
|
STALE_EVENT_AGE = 86400 # 24 hours in seconds
|
|
STALE_STARTING_AGE = 3600 # 1 hour - sessions stuck in "starting" are orphans
|
|
CODEX_ACTIVE_WINDOW = 600 # 10 minutes - only discover recently-active Codex sessions
|
|
|
|
# Cache for Zellij session list (avoid calling zellij on every request)
|
|
_zellij_cache = {"sessions": None, "expires": 0}
|
|
|
|
# Cache for Codex pane info (avoid running pgrep/ps/lsof on every request)
|
|
_codex_pane_cache = {"pid_info": {}, "cwd_map": {}, "expires": 0}
|
|
|
|
# Cache for parsed context usage by transcript file path + mtime/size
|
|
# Limited to prevent unbounded memory growth
|
|
_context_usage_cache = {}
|
|
_CONTEXT_CACHE_MAX = 100
|
|
|
|
# Cache mapping Codex session IDs to transcript paths (or None when missing)
|
|
_codex_transcript_cache = {}
|
|
_CODEX_CACHE_MAX = 200
|
|
|
|
# Codex sessions dismissed during this server lifetime (prevents re-discovery)
|
|
_dismissed_codex_ids = set()
|
|
_DISMISSED_MAX = 500
|
|
|
|
# Serialize state collection because it mutates session files/caches.
|
|
_state_lock = threading.Lock()
|