Skip to content

Commit 7c5ca6a

Browse files
committed
fix(plugin): close TUI sidebar pane on session stop (#1109)
- Add _close_tui_sidebar() to stop.py that finds and kills codingbuddy tmux panes via list-panes + kill-pane - Call it at the end of handle_stop() with exception safety - Add 4 tests covering no-tmux, kill, no-match, and exception cases Closes #1109
1 parent 246e225 commit 7c5ca6a

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

packages/claude-code-plugin/hooks/stop.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66
import json
77
import os
8+
import subprocess
89
import sys
910

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

179+
# Close TUI sidebar pane (#1109)
180+
try:
181+
_close_tui_sidebar()
182+
except Exception:
183+
pass # Never block session stop
184+
178185
if summary:
179186
return {
180187
"systemMessage": summary,
@@ -285,6 +292,26 @@ def _box(text):
285292
return "\n".join(lines)
286293

287294

295+
def _close_tui_sidebar():
296+
"""Close codingbuddy TUI sidebar pane on session stop (#1109)."""
297+
if not os.environ.get("TMUX"):
298+
return
299+
try:
300+
result = subprocess.run(
301+
["tmux", "list-panes", "-F", "#{pane_id} #{pane_current_command}"],
302+
capture_output=True, text=True, timeout=2,
303+
)
304+
for line in result.stdout.strip().splitlines():
305+
parts = line.split(None, 1)
306+
if len(parts) == 2 and "codingbuddy" in parts[1]:
307+
subprocess.run(
308+
["tmux", "kill-pane", "-t", parts[0]],
309+
capture_output=True, timeout=2,
310+
)
311+
except Exception:
312+
pass # Never block session stop
313+
314+
288315
def _maybe_notify_session_end(summary: str):
289316
"""Send session summary notification if configured."""
290317
if not summary:

packages/claude-code-plugin/tests/test_stop.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,49 @@ def test_analysis_failure_does_not_block_stop(
229229
assert "Auto-Learning" not in result["systemMessage"]
230230

231231

232+
class TestCloseTuiSidebar:
233+
"""Tests for TUI sidebar cleanup on session stop (#1109)."""
234+
235+
def test_noop_when_not_in_tmux(self, monkeypatch):
236+
"""Should do nothing when TMUX env var is not set."""
237+
monkeypatch.delenv("TMUX", raising=False)
238+
mod = _load_module()
239+
# Should not raise
240+
mod._close_tui_sidebar()
241+
242+
@patch("subprocess.run")
243+
def test_kills_codingbuddy_pane(self, mock_run, monkeypatch):
244+
"""Should kill panes running codingbuddy."""
245+
monkeypatch.setenv("TMUX", "/tmp/tmux-501/default,12345,0")
246+
mock_run.return_value = MagicMock(
247+
stdout="%1 zsh\n%2 codingbuddy\n%3 vim\n",
248+
)
249+
mod = _load_module()
250+
mod._close_tui_sidebar()
251+
252+
assert mock_run.call_count == 2 # list-panes + kill-pane
253+
kill_call = mock_run.call_args_list[1]
254+
assert kill_call[0][0] == ["tmux", "kill-pane", "-t", "%2"]
255+
256+
@patch("subprocess.run")
257+
def test_no_kill_when_no_codingbuddy_pane(self, mock_run, monkeypatch):
258+
"""Should not call kill-pane when no codingbuddy pane exists."""
259+
monkeypatch.setenv("TMUX", "/tmp/tmux-501/default,12345,0")
260+
mock_run.return_value = MagicMock(stdout="%1 zsh\n%2 vim\n")
261+
mod = _load_module()
262+
mod._close_tui_sidebar()
263+
264+
assert mock_run.call_count == 1 # only list-panes
265+
266+
@patch("subprocess.run", side_effect=Exception("tmux crashed"))
267+
def test_exception_does_not_propagate(self, mock_run, monkeypatch):
268+
"""Should swallow all exceptions."""
269+
monkeypatch.setenv("TMUX", "/tmp/tmux-501/default,12345,0")
270+
mod = _load_module()
271+
# Should not raise
272+
mod._close_tui_sidebar()
273+
274+
232275
class TestSessionSummaryRendering:
233276
"""Tests for buddy session summary rendering in stop hook (#972)."""
234277

0 commit comments

Comments
 (0)