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
19 changes: 16 additions & 3 deletions factory/agents/prompts/refactory.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,24 @@ Periodically trigger playbook evolution via `factory ace` to distill experiment

### Input Submission

Always use `C-m` (not `Enter`) when sending keys to tmux sessions running Claude Code:
Use the two-step hex approach when sending keys to tmux sessions running Claude Code — send text first, then Enter as hex `0d`:
```bash
tmux send-keys -t <session> "your input" C-m
tmux send-keys -t <session> -- "your input"
tmux send-keys -t <session> -H 0d
```
`Enter` is unreliable inside Claude Code sessions — `C-m` is the canonical carriage return and works consistently.
This avoids ambiguity with `C-m` and `Enter` key interpretation inside Claude Code sessions.

### Vi Mode Handling

When Claude Code has vi/vim editor mode enabled (`editorMode: "vim"` in `~/.claude/settings.json`), the user may be in normal mode. Send Escape → wait 100ms → `i` before text to ensure insert mode:
```bash
tmux send-keys -t <session> Escape
sleep 0.1
tmux send-keys -t <session> i
tmux send-keys -t <session> -- "your input"
tmux send-keys -t <session> -H 0d
```
`factory tmux` handles this automatically — it detects vi mode from user settings and prefixes the Escape+i sequence when needed.

### Post-Dispatch Verification

Expand Down
19 changes: 16 additions & 3 deletions factory/agents/skills/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,24 @@ If the session exists but the CEO process has exited, the session is stale — s

### Sending Input to Sessions

Always use `C-m` (not `Enter`) when sending keys to tmux sessions running Claude Code:
Use the two-step hex approach when sending keys to tmux sessions running Claude Code — send text first, then Enter as hex `0d`:
```bash
tmux send-keys -t <session_name> "your input" C-m
tmux send-keys -t <session_name> -- "your input"
tmux send-keys -t <session_name> -H 0d
```
`Enter` is unreliable inside Claude Code sessions — `C-m` is the canonical carriage return.
This avoids ambiguity with `C-m` and `Enter` key interpretation inside Claude Code sessions.

### Vi Mode Handling

When Claude Code has vi/vim editor mode enabled (`editorMode: "vim"` in `~/.claude/settings.json`), prefix with Escape → 100ms delay → `i` to ensure insert mode:
```bash
tmux send-keys -t <session_name> Escape
sleep 0.1
tmux send-keys -t <session_name> i
tmux send-keys -t <session_name> -- "your input"
tmux send-keys -t <session_name> -H 0d
```
`factory tmux` handles this automatically — it detects vi mode from user settings and applies the prefix when needed.

### Capturing Output

Expand Down
32 changes: 28 additions & 4 deletions factory/runners/_tmux_persist.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@
_WINDOW_POLL_TIMEOUT = 3.0


def _detect_vim_mode() -> bool:
"""Check if Claude Code has vi/vim editor mode enabled in user settings."""
try:
settings_path = Path.home() / ".claude" / "settings.json"
settings = json.loads(settings_path.read_text())
return settings.get("editorMode") == "vim"
except (json.JSONDecodeError, OSError):
logger.debug("vim mode detection failed, defaulting to normal mode")
return False


def _tmux_send_enter(session_window: str, text: str, *, vim_mode: bool = False) -> None:
"""Send text followed by Enter (hex 0d) to a tmux target.

When vim_mode is True, prefix with Escape + 100ms delay + i to ensure
the target pane is in insert mode before sending text.
"""
if vim_mode:
subprocess.run(["tmux", "send-keys", "-t", session_window, "Escape"], capture_output=True)
time.sleep(0.1)
subprocess.run(["tmux", "send-keys", "-t", session_window, "i"], capture_output=True)
subprocess.run(["tmux", "send-keys", "-t", session_window, "--", text], capture_output=True)
subprocess.run(["tmux", "send-keys", "-t", session_window, "-H", "0d"], capture_output=True)


def find_project_path(cwd: Path) -> Path:
"""Find the project root by walking up from cwd looking for .factory/."""
path = cwd.resolve()
Expand Down Expand Up @@ -153,6 +178,8 @@ async def run_in_tmux(

Returns (stdout, return_code, None). Usage is always None for tmux mode.
"""
vim_mode = _detect_vim_mode()

run_id = uuid.uuid4().hex[:8]
path_hash = hashlib.sha1(str(project_path).encode()).hexdigest()[:6]
session = f"{_SESSION_PREFIX}{project_path.name}-{path_hash}"
Expand Down Expand Up @@ -230,10 +257,7 @@ async def run_in_tmux(
_cleanup(tmpdir)
return f"Agent timed out after {timeout}s", 1, None

subprocess.run(
["tmux", "send-keys", "-t", f"{session}:{window}", "/exit", "C-m"],
capture_output=True,
)
_tmux_send_enter(f"{session}:{window}", "/exit", vim_mode=vim_mode)
await _wait_for_window_exit(session, window)
if _window_exists(session, window):
subprocess.run(
Expand Down
Loading
Loading