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
27 changes: 27 additions & 0 deletions packages/claude-code-plugin/hooks/stop.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""
import json
import os
import subprocess
import sys

# Resolve hooks/lib and add to path
Expand Down Expand Up @@ -175,6 +176,12 @@ def handle_stop(data: dict):
except Exception:
pass # Never block session stop

# Close TUI sidebar pane (#1109)
try:
_close_tui_sidebar()
except Exception:
pass # Never block session stop

if summary:
return {
"systemMessage": summary,
Expand Down Expand Up @@ -285,6 +292,26 @@ def _box(text):
return "\n".join(lines)


def _close_tui_sidebar():
"""Close codingbuddy TUI sidebar pane on session stop (#1109)."""
if not os.environ.get("TMUX"):
return
try:
result = subprocess.run(
["tmux", "list-panes", "-F", "#{pane_id} #{pane_current_command}"],
capture_output=True, text=True, timeout=2,
)
for line in result.stdout.strip().splitlines():
parts = line.split(None, 1)
if len(parts) == 2 and "codingbuddy" in parts[1]:
subprocess.run(
["tmux", "kill-pane", "-t", parts[0]],
capture_output=True, timeout=2,
)
except Exception:
pass # Never block session stop


def _maybe_notify_session_end(summary: str):
"""Send session summary notification if configured."""
if not summary:
Expand Down
43 changes: 43 additions & 0 deletions packages/claude-code-plugin/tests/test_stop.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,49 @@ def test_analysis_failure_does_not_block_stop(
assert "Auto-Learning" not in result["systemMessage"]


class TestCloseTuiSidebar:
"""Tests for TUI sidebar cleanup on session stop (#1109)."""

def test_noop_when_not_in_tmux(self, monkeypatch):
"""Should do nothing when TMUX env var is not set."""
monkeypatch.delenv("TMUX", raising=False)
mod = _load_module()
# Should not raise
mod._close_tui_sidebar()

@patch("subprocess.run")
def test_kills_codingbuddy_pane(self, mock_run, monkeypatch):
"""Should kill panes running codingbuddy."""
monkeypatch.setenv("TMUX", "/tmp/tmux-501/default,12345,0")
mock_run.return_value = MagicMock(
stdout="%1 zsh\n%2 codingbuddy\n%3 vim\n",
)
mod = _load_module()
mod._close_tui_sidebar()

assert mock_run.call_count == 2 # list-panes + kill-pane
kill_call = mock_run.call_args_list[1]
assert kill_call[0][0] == ["tmux", "kill-pane", "-t", "%2"]

@patch("subprocess.run")
def test_no_kill_when_no_codingbuddy_pane(self, mock_run, monkeypatch):
"""Should not call kill-pane when no codingbuddy pane exists."""
monkeypatch.setenv("TMUX", "/tmp/tmux-501/default,12345,0")
mock_run.return_value = MagicMock(stdout="%1 zsh\n%2 vim\n")
mod = _load_module()
mod._close_tui_sidebar()

assert mock_run.call_count == 1 # only list-panes

@patch("subprocess.run", side_effect=Exception("tmux crashed"))
def test_exception_does_not_propagate(self, mock_run, monkeypatch):
"""Should swallow all exceptions."""
monkeypatch.setenv("TMUX", "/tmp/tmux-501/default,12345,0")
mod = _load_module()
# Should not raise
mod._close_tui_sidebar()


class TestSessionSummaryRendering:
"""Tests for buddy session summary rendering in stop hook (#972)."""

Expand Down
Loading