Skip to content
Merged
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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Recommended settings:

```tmux
set -g @coding-agents-tmux-provider 'plugin'
set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude'
set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot'
set -g @coding-agents-tmux-menu-key 'O'
set -g @coding-agents-tmux-popup-key 'P'
set -g @coding-agents-tmux-waiting-menu-key 'W'
Expand Down Expand Up @@ -125,7 +125,7 @@ It also installs or updates Codex hook integration under:
~/.codex/hooks.json
```

With the recommended `@coding-agents-tmux-auto-install 'opencode,pi,codex,claude'` setting, it also installs or updates Claude Code hook integration under:
With the recommended `@coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot'` setting, it also installs or updates Claude Code hook integration under:

```text
~/.claude/settings.json
Expand Down Expand Up @@ -160,10 +160,10 @@ You can also control all tmux-managed installs together:
```tmux
set -g @coding-agents-tmux-auto-install 'auto'
set -g @coding-agents-tmux-auto-install 'off'
set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude'
set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot'
```

The explicit `opencode,pi,codex,claude` list is the recommended README setting because it makes the intended managed installs obvious in your tmux config. When `@coding-agents-tmux-auto-install` is set, it takes precedence over the individual install toggles.
The explicit `opencode,pi,codex,claude,copilot` list is the recommended README setting because it makes the intended managed installs obvious in your tmux config. When `@coding-agents-tmux-auto-install` is set, it takes precedence over the individual install toggles. Kiro CLI has no hook/extension installer; it uses pane and preview detection without an install step.

## Usage

Expand Down Expand Up @@ -468,7 +468,7 @@ set -g @coding-agents-tmux-install-claude-hooks 'on'
or the shared selector:

```tmux
set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude'
set -g @coding-agents-tmux-auto-install 'opencode,pi,codex,claude,copilot'
```

3. Optionally inspect the managed hook template before merging it into project or user Claude settings:
Expand Down Expand Up @@ -498,7 +498,7 @@ This means any `kiro-cli` pane can be discovered and switched to without naming

An interactive `copilot` process (or `copilot.exe` on Windows) in a tmux pane is discovered in `list`, the chooser/popup, and `status`. The `gh copilot` launcher is also discovered when its foreground process group contains a direct Copilot CLI child (or a known npm Node wrapper with a Copilot child); GitHub CLI's downloaded copy does not need a separate standalone installation. Use `--agent copilot` to filter navigation. No hooks are required: without them, activity and status remain `unknown` rather than guessing a wait. Title text, unrelated `gh` commands, and arbitrary `copilot-*` commands are not detected. The npm-installed CLI's Node wrapper is recognized by its executable path; generic wrapper support is tracked separately in #19.

For optional, **local Copilot CLI only** turn and prompt state, preview the configuration with `coding-agents-tmux copilot-hooks-template`, then explicitly run `coding-agents-tmux install-copilot`. This creates only `${COPILOT_HOME:-~/.copilot}/hooks/coding-agents-tmux.json`; other user hooks are untouched. Restart existing Copilot CLI sessions so hooks load. Do not put these hooks in `.github/hooks/` (repository hooks also execute in Copilot cloud agent). The tmux plugin never auto-installs Copilot hooks.
For optional, **local Copilot CLI only** turn and prompt state, add `copilot` to `@coding-agents-tmux-auto-install` (or use `auto`) to install/update the user-level hooks whenever the tmux plugin loads. You can preview the configuration with `coding-agents-tmux copilot-hooks-template` or install it directly with `coding-agents-tmux install-copilot`. This creates only `${COPILOT_HOME:-~/.copilot}/hooks/coding-agents-tmux.json`; other user hooks are untouched. Restart existing Copilot CLI sessions so hooks load. Do not put these hooks in `.github/hooks/` (repository hooks also execute in Copilot cloud agent). Leaving `copilot` out of the selector does not install hooks.

The adapter correlates `TMUX_PANE`, the hook process's Copilot parent PID, and the Copilot `sessionId`, including concurrent sessions in one directory and `/clear` (which ends the old session and starts another). An exited or backgrounded Copilot process invalidates its hook state even if another CLI starts in the same pane. `userPromptSubmitted` means running; `agentStop` means idle. Only `notification` types `permission_prompt` and `elicitation_dialog` assert waiting-question and waiting-input respectively; automatic permission checks and `preToolUse` never assert a wait. An elicitation notification does not reveal whether its form contains multiple choices, so it is conservatively labeled input. Resolved prompts clear at the next turn boundary (`agentStop`); hooks do not expose an immediate resolution event, so the state can briefly lag after an answer. Hook state expires after 60 seconds without a subsequent event (including an unanswered prompt), reverting to the coarse navigable state instead of retaining a false wait. `inspect` reports hook source, age and expiry or command-only fallback. If hooks are disabled, folder trust blocks them, tmux pane identity is missing, or a hook is missed, no directory-only match is made. `COPILOT_HOME` selects the hook directory; `CODING_AGENTS_TMUX_COPILOT_STATE_DIR` overrides the local state directory. The integration neither grants permissions nor injects context.

Expand Down
18 changes: 17 additions & 1 deletion coding-agents-tmux.tmux
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ install_claude_hooks() {
fi
}

install_copilot_hooks() {
if ! "$CURRENT_DIR/bin/coding-agents-tmux" install-copilot >/dev/null 2>&1; then
tmux display-message "coding-agents-tmux: failed to install Copilot CLI hook configuration"
fi
}

install_pi_extension() {
local extension_source pi_dir extension_dir extension_target existing_target installed_changed

Expand Down Expand Up @@ -395,7 +401,7 @@ install_pi_extension() {
}

main() {
local menu_key popup_key waiting_menu_key waiting_popup_key provider server_map popup_filter popup_width popup_height popup_title status_enabled status_style status_position status_option status_interval status_mode install_plugin install_codex install_pi install_claude auto_install_value status_text_segment status_inline_segment status_tone_segment status_refresh_command
local menu_key popup_key waiting_menu_key waiting_popup_key provider server_map popup_filter popup_width popup_height popup_title status_enabled status_style status_position status_option status_interval status_mode install_plugin install_codex install_pi install_claude install_copilot auto_install_value status_text_segment status_inline_segment status_tone_segment status_refresh_command
local status_prefix status_color_neutral status_color_busy status_color_waiting status_color_idle status_color_unknown notify_command
local previous_status_segment previous_status_option previous_menu_key previous_popup_key previous_waiting_menu_key previous_waiting_popup_key
menu_key="$(normalize_binding_key "$(get_tmux_option '@coding-agents-tmux-menu-key' 'O')")"
Expand All @@ -418,18 +424,21 @@ main() {
install_codex='on'
install_pi='on'
install_claude='on'
install_copilot='on'
Comment thread
corwinm marked this conversation as resolved.
Resolved
;;
off|"")
install_plugin='off'
install_codex='off'
install_pi='off'
install_claude='off'
install_copilot='off'
;;
*)
install_plugin='off'
install_codex='off'
install_pi='off'
install_claude='off'
install_copilot='off'

if auto_install_includes "$auto_install_value" 'opencode'; then
install_plugin='on'
Expand All @@ -446,13 +455,17 @@ main() {
if auto_install_includes "$auto_install_value" 'claude'; then
install_claude='on'
fi
if auto_install_includes "$auto_install_value" 'copilot'; then
install_copilot='on'
fi
;;
esac
else
install_plugin="$(normalize_toggle "$(get_tmux_option '@coding-agents-tmux-install-opencode-plugin' 'on')")"
install_codex="$(normalize_toggle "$(get_tmux_option '@coding-agents-tmux-install-codex-hooks' 'on')")"
install_pi="$(normalize_toggle "$(get_tmux_option '@coding-agents-tmux-install-pi-extension' 'on')")"
install_claude="$(normalize_toggle "$(get_tmux_option '@coding-agents-tmux-install-claude-hooks' 'off')")"
install_copilot='off'
fi
status_enabled="$(get_tmux_option '@coding-agents-tmux-status' 'on')"
status_style="$(get_tmux_option '@coding-agents-tmux-status-style' 'tmux')"
Expand Down Expand Up @@ -497,6 +510,9 @@ main() {
if [ "$install_claude" = "on" ]; then
install_claude_hooks
fi
if [ "$install_copilot" = "on" ]; then
install_copilot_hooks
fi

local popup_filter_arg=""
case "$popup_filter" in
Expand Down
4 changes: 3 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,9 @@ async function main(): Promise<void> {

program
.command("install-copilot")
.description("Explicitly install Copilot CLI user-level hooks; never run automatically")
.description(
"Install Copilot CLI user-level hooks directly or through opt-in tmux auto-install",
)
.action(runInstallCopilotCommand);

program
Expand Down
4 changes: 4 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,10 @@ test("CLI help and tmux-config work through the entrypoint script", async () =>
assert.equal(helpResult.exitCode, 0);
assert.match(helpResult.stdoutText, /Usage: coding-agents-tmux/);
assert.match(helpResult.stdoutText, /tmux-config/);
const copilotHelp = await runCommand([BIN_PATH, "install-copilot", "--help"]);
assert.equal(copilotHelp.exitCode, 0);
assert.match(copilotHelp.stdoutText, /opt-in tmux\s+auto-install/);
assert.doesNotMatch(copilotHelp.stdoutText, /never run automatically/);
assert.equal(configResult.exitCode, 0);
assert.match(configResult.stdoutText, /# >>> coding-agents-tmux >>>/);
assert.match(configResult.stdoutText, /--provider/);
Expand Down
21 changes: 19 additions & 2 deletions test/tmux-plugin-rename.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
mkdtempSync,
readlinkSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
Expand Down Expand Up @@ -304,7 +305,7 @@ exit 1
}
});

test("coding-agents-tmux.tmux honors @coding-agents-tmux-auto-install lists including claude", async () => {
test("coding-agents-tmux.tmux honors @coding-agents-tmux-auto-install lists including claude and copilot", async () => {
const fakeTmux = installFakeTmux(`
log_path='__LOG_PATH__'
option="\${!#}"
Expand All @@ -319,7 +320,7 @@ show-option)
printf 'off\n'
;;
@coding-agents-tmux-auto-install)
printf 'pi,claude\n'
printf '%s\n' "\${AUTO_INSTALL_SELECTION:-pi,claude,copilot}"
;;
esac
exit 0
Expand All @@ -338,6 +339,7 @@ exit 1
const piHome = join(home, ".pi-home");
const codexHome = join(home, ".codex-home");
const claudeHome = join(home, ".claude-home");
const copilotHome = join(home, ".copilot-home");
installFakeNpm(fakeTmux.pathEntry);
const restoreEnv = setEnv({
HOME: home,
Expand All @@ -346,22 +348,37 @@ exit 1
PI_CODING_AGENT_DIR: piHome,
CODEX_HOME: codexHome,
CLAUDE_HOME: claudeHome,
COPILOT_HOME: copilotHome,
AUTO_INSTALL_SELECTION: "pi,claude",
});

try {
const result = await runCommand([join(process.cwd(), "coding-agents-tmux.tmux")]);
const newPluginPath = join(configHome, "opencode", "plugins", "coding-agents-tmux.ts");
const newPiExtensionPath = join(piHome, "extensions", "coding-agents-tmux", "index.ts");
const claudeSettingsPath = join(claudeHome, "settings.json");
const copilotHooksPath = join(copilotHome, "hooks", "coding-agents-tmux.json");
const codexHooksPath = join(codexHome, "hooks.json");

assert.equal(result.exitCode, 0);
assert.equal(result.stderrText.trim(), "");
assert.equal(existsSync(copilotHooksPath), false);
process.env.AUTO_INSTALL_SELECTION = "pi,claude,copilot";
const optedIn = await runCommand([join(process.cwd(), "coding-agents-tmux.tmux")]);
assert.equal(optedIn.exitCode, 0);
assert.equal(optedIn.stderrText.trim(), "");
assert.equal(existsSync(newPluginPath), false);
assert.ok(existsSync(newPiExtensionPath));
assert.ok(existsSync(claudeSettingsPath));
assert.ok(existsSync(copilotHooksPath));
assert.equal(existsSync(codexHooksPath), false);
assert.match(readFileSync(claudeSettingsPath, "utf8"), /claude-hook-state/);
assert.match(readFileSync(copilotHooksPath, "utf8"), /copilot-hook-state/);
rmSync(copilotHooksPath);
process.env.AUTO_INSTALL_SELECTION = "auto";
const all = await runCommand([join(process.cwd(), "coding-agents-tmux.tmux")]);
assert.equal(all.exitCode, 0);
assert.ok(existsSync(copilotHooksPath));
} finally {
restoreEnv();
}
Expand Down
Loading