overcode 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. overcode/__init__.py +1 -1
  2. overcode/cli.py +154 -51
  3. overcode/config.py +66 -0
  4. overcode/daemon_claude_skill.md +36 -33
  5. overcode/history_reader.py +69 -8
  6. overcode/implementations.py +178 -87
  7. overcode/monitor_daemon.py +87 -97
  8. overcode/monitor_daemon_core.py +261 -0
  9. overcode/monitor_daemon_state.py +24 -15
  10. overcode/pid_utils.py +17 -3
  11. overcode/session_manager.py +54 -0
  12. overcode/settings.py +34 -0
  13. overcode/status_constants.py +1 -1
  14. overcode/status_detector.py +8 -2
  15. overcode/status_patterns.py +19 -0
  16. overcode/summarizer_client.py +72 -27
  17. overcode/summarizer_component.py +87 -107
  18. overcode/supervisor_daemon.py +55 -38
  19. overcode/supervisor_daemon_core.py +210 -0
  20. overcode/testing/__init__.py +6 -0
  21. overcode/testing/renderer.py +268 -0
  22. overcode/testing/tmux_driver.py +223 -0
  23. overcode/testing/tui_eye.py +185 -0
  24. overcode/testing/tui_eye_skill.md +187 -0
  25. overcode/tmux_manager.py +117 -93
  26. overcode/tui.py +399 -1969
  27. overcode/tui_actions/__init__.py +20 -0
  28. overcode/tui_actions/daemon.py +201 -0
  29. overcode/tui_actions/input.py +128 -0
  30. overcode/tui_actions/navigation.py +117 -0
  31. overcode/tui_actions/session.py +428 -0
  32. overcode/tui_actions/view.py +357 -0
  33. overcode/tui_helpers.py +42 -9
  34. overcode/tui_logic.py +347 -0
  35. overcode/tui_render.py +414 -0
  36. overcode/tui_widgets/__init__.py +24 -0
  37. overcode/tui_widgets/command_bar.py +399 -0
  38. overcode/tui_widgets/daemon_panel.py +153 -0
  39. overcode/tui_widgets/daemon_status_bar.py +245 -0
  40. overcode/tui_widgets/help_overlay.py +71 -0
  41. overcode/tui_widgets/preview_pane.py +69 -0
  42. overcode/tui_widgets/session_summary.py +514 -0
  43. overcode/tui_widgets/status_timeline.py +253 -0
  44. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/METADATA +4 -1
  45. overcode-0.1.4.dist-info/RECORD +68 -0
  46. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/WHEEL +1 -1
  47. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
  48. overcode-0.1.2.dist-info/RECORD +0 -45
  49. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
  50. {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,185 @@
1
+ """tui-eye: Visual TUI testing tool for Claude Code.
2
+
3
+ This CLI tool allows Claude Code to "see" TUI applications by:
4
+ 1. Running them in a controlled tmux session
5
+ 2. Capturing screenshots as PNG images
6
+ 3. Sending keystrokes for interaction
7
+
8
+ Example usage:
9
+ tui-eye start "overcode supervisor" --size 120x40
10
+ tui-eye screenshot /tmp/tui.png
11
+ tui-eye send j j enter
12
+ tui-eye wait-for "Session:"
13
+ tui-eye stop
14
+ """
15
+
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Annotated, Optional
19
+ import typer
20
+
21
+ from .tmux_driver import TUIDriver
22
+ from .renderer import render_terminal_to_png
23
+
24
+ app = typer.Typer(
25
+ name="tui-eye",
26
+ help="Visual TUI testing tool - gives Claude Code 'eyes' into TUI apps",
27
+ )
28
+
29
+ # Global driver instance (persists between commands via state file)
30
+ STATE_FILE = Path("/tmp/tui-eye-state")
31
+ DEFAULT_SESSION = "tui-eye"
32
+
33
+
34
+ def _get_driver() -> TUIDriver:
35
+ """Get or create a TUI driver instance."""
36
+ session_name = DEFAULT_SESSION
37
+ if STATE_FILE.exists():
38
+ session_name = STATE_FILE.read_text().strip() or DEFAULT_SESSION
39
+ return TUIDriver(session_name=session_name)
40
+
41
+
42
+ def _save_state(session_name: str) -> None:
43
+ """Save the current session name to state file."""
44
+ STATE_FILE.write_text(session_name)
45
+
46
+
47
+ def _clear_state() -> None:
48
+ """Clear the state file."""
49
+ if STATE_FILE.exists():
50
+ STATE_FILE.unlink()
51
+
52
+
53
+ @app.command()
54
+ def start(
55
+ command: Annotated[str, typer.Argument(help="Command to run in the TUI")],
56
+ size: Annotated[str, typer.Option(help="Terminal size as WIDTHxHEIGHT")] = "220x40",
57
+ session: Annotated[str, typer.Option(help="tmux session name")] = DEFAULT_SESSION,
58
+ ) -> None:
59
+ """Start a TUI application in a tmux session."""
60
+ # Parse size
61
+ try:
62
+ width, height = map(int, size.lower().split("x"))
63
+ except ValueError:
64
+ typer.echo(f"Error: Invalid size format '{size}'. Use WIDTHxHEIGHT (e.g., 120x40)")
65
+ raise typer.Exit(1)
66
+
67
+ driver = TUIDriver(session_name=session)
68
+
69
+ typer.echo(f"Starting TUI: {command}")
70
+ typer.echo(f"Size: {width}x{height}")
71
+ typer.echo(f"Session: {session}")
72
+
73
+ driver.start(command, width=width, height=height)
74
+ _save_state(session)
75
+
76
+ typer.echo("TUI started. Use 'tui-eye screenshot' to capture.")
77
+
78
+
79
+ @app.command()
80
+ def stop() -> None:
81
+ """Stop the TUI session and clean up."""
82
+ driver = _get_driver()
83
+ driver.stop()
84
+ _clear_state()
85
+ typer.echo("TUI session stopped.")
86
+
87
+
88
+ @app.command()
89
+ def screenshot(
90
+ output: Annotated[
91
+ str, typer.Argument(help="Output PNG file path")
92
+ ] = "/tmp/tui-screenshot.png",
93
+ width: Annotated[int, typer.Option(help="Terminal width for rendering")] = 220,
94
+ height: Annotated[int, typer.Option(help="Terminal height for rendering")] = 45,
95
+ ) -> None:
96
+ """Capture a screenshot of the TUI as a PNG image."""
97
+ driver = _get_driver()
98
+
99
+ if not driver.is_running:
100
+ typer.echo("Error: No TUI session running. Use 'tui-eye start' first.")
101
+ raise typer.Exit(1)
102
+
103
+ # Capture with ANSI codes
104
+ content = driver.capture(with_ansi=True)
105
+
106
+ # Render to PNG
107
+ output_path = render_terminal_to_png(
108
+ content,
109
+ output,
110
+ width=width,
111
+ height=height,
112
+ )
113
+
114
+ typer.echo(f"Screenshot saved: {output_path}")
115
+
116
+
117
+ @app.command()
118
+ def capture(
119
+ text: Annotated[bool, typer.Option("--text", help="Output plain text (no ANSI)")] = False,
120
+ ) -> None:
121
+ """Capture and print the current screen content."""
122
+ driver = _get_driver()
123
+
124
+ if not driver.is_running:
125
+ typer.echo("Error: No TUI session running. Use 'tui-eye start' first.")
126
+ raise typer.Exit(1)
127
+
128
+ content = driver.capture(with_ansi=not text)
129
+ typer.echo(content)
130
+
131
+
132
+ @app.command()
133
+ def send(
134
+ keys: Annotated[list[str], typer.Argument(help="Keys to send (e.g., j k enter)")],
135
+ ) -> None:
136
+ """Send keystrokes to the TUI."""
137
+ driver = _get_driver()
138
+
139
+ if not driver.is_running:
140
+ typer.echo("Error: No TUI session running. Use 'tui-eye start' first.")
141
+ raise typer.Exit(1)
142
+
143
+ driver.send_keys(*keys)
144
+ typer.echo(f"Sent keys: {' '.join(keys)}")
145
+
146
+
147
+ @app.command("wait-for")
148
+ def wait_for(
149
+ text: Annotated[str, typer.Argument(help="Text to wait for")],
150
+ timeout: Annotated[float, typer.Option(help="Timeout in seconds")] = 10.0,
151
+ ) -> None:
152
+ """Wait for specific text to appear on screen."""
153
+ driver = _get_driver()
154
+
155
+ if not driver.is_running:
156
+ typer.echo("Error: No TUI session running. Use 'tui-eye start' first.")
157
+ raise typer.Exit(1)
158
+
159
+ typer.echo(f"Waiting for: '{text}' (timeout: {timeout}s)")
160
+
161
+ if driver.wait_for(text, timeout=timeout):
162
+ typer.echo("Found!")
163
+ else:
164
+ typer.echo(f"Timeout: text '{text}' not found after {timeout}s")
165
+ raise typer.Exit(1)
166
+
167
+
168
+ @app.command()
169
+ def status() -> None:
170
+ """Check the status of the TUI session."""
171
+ driver = _get_driver()
172
+
173
+ if driver.is_running:
174
+ typer.echo(f"Session '{driver.session_name}' is running.")
175
+ else:
176
+ typer.echo("No TUI session running.")
177
+
178
+
179
+ def main() -> None:
180
+ """Main entry point."""
181
+ app()
182
+
183
+
184
+ if __name__ == "__main__":
185
+ main()
@@ -0,0 +1,187 @@
1
+ # TUI Eye - Visual TUI Testing Skill
2
+
3
+ ```yaml
4
+ ---
5
+ name: tui-eye
6
+ description: Interactive visual testing of TUI applications. Use when testing the overcode supervisor TUI, validating layouts, or running smoke tests.
7
+ disable-model-invocation: true
8
+ ---
9
+ ```
10
+
11
+ You are performing visual TUI testing using the `tui-eye` tool. This tool gives you "eyes" into TUI applications by capturing screenshots as PNG images that you can read and analyze.
12
+
13
+ ## Core Commands
14
+
15
+ ```bash
16
+ # Start a TUI in a controlled tmux session (220x40 default)
17
+ tui-eye start "overcode monitor" --size 220x40
18
+
19
+ # Capture screenshot for visual inspection
20
+ tui-eye screenshot /tmp/tui.png
21
+
22
+ # Read the screenshot (use Claude Code's Read tool)
23
+ # Then analyze: layout, alignment, colors, text overflow, etc.
24
+
25
+ # Send keystrokes
26
+ tui-eye send j j enter # Navigate down twice, press enter
27
+ tui-eye send h # Toggle help overlay
28
+ tui-eye send escape # Close dialogs/cancel
29
+
30
+ # Wait for content to appear
31
+ tui-eye wait-for "Session:" --timeout 10
32
+
33
+ # Get text-only capture (for searching/assertions)
34
+ tui-eye capture --text
35
+
36
+ # Check session status
37
+ tui-eye status
38
+
39
+ # Clean up when done
40
+ tui-eye stop
41
+ ```
42
+
43
+ ## Workflow: Visual Testing
44
+
45
+ 1. **Start the TUI**
46
+ ```bash
47
+ tui-eye start "overcode monitor" --size 220x40
48
+ ```
49
+
50
+ 2. **Capture & Analyze**
51
+ ```bash
52
+ tui-eye screenshot /tmp/check.png
53
+ ```
54
+ Then read `/tmp/check.png` and visually inspect:
55
+ - Is the layout correct?
56
+ - Are columns aligned?
57
+ - Is text truncated or wrapped unexpectedly?
58
+ - Are colors/status indicators showing correctly?
59
+
60
+ 3. **Interact**
61
+ ```bash
62
+ tui-eye send j # Navigate
63
+ tui-eye send enter # Select/confirm
64
+ tui-eye send h # Toggle help
65
+ ```
66
+
67
+ 4. **Verify Changes**
68
+ ```bash
69
+ tui-eye screenshot /tmp/after.png
70
+ ```
71
+ Compare to expected state.
72
+
73
+ 5. **Clean Up**
74
+ ```bash
75
+ tui-eye stop
76
+ ```
77
+
78
+ ## Key Mappings
79
+
80
+ | Key | tmux Name | Description |
81
+ |-----|-----------|-------------|
82
+ | `j` | j | Navigate down |
83
+ | `k` | k | Navigate up |
84
+ | `enter` | Enter | Confirm/select |
85
+ | `escape` | Escape | Cancel/close |
86
+ | `h` | h | Toggle help |
87
+ | `q` | q | Quit (some TUIs) |
88
+ | `tab` | Tab | Next field |
89
+ | `space` | Space | Toggle/expand |
90
+
91
+ ## Example: Smoke Test
92
+
93
+ ```bash
94
+ # Start supervisor TUI
95
+ tui-eye start "overcode monitor" --size 220x45
96
+
97
+ # Wait for initial render
98
+ tui-eye wait-for "Timeline:" --timeout 10
99
+
100
+ # Capture initial state
101
+ tui-eye screenshot /tmp/smoke-1.png
102
+ # [Read /tmp/smoke-1.png - verify layout looks correct]
103
+
104
+ # Test help overlay
105
+ tui-eye send h
106
+ tui-eye screenshot /tmp/smoke-help.png
107
+ # [Read - verify help is displayed]
108
+
109
+ tui-eye send h
110
+ tui-eye screenshot /tmp/smoke-help-closed.png
111
+ # [Read - verify help closed, main view restored]
112
+
113
+ # Navigate if there are sessions
114
+ tui-eye send j j
115
+ tui-eye screenshot /tmp/smoke-nav.png
116
+ # [Read - verify navigation worked]
117
+
118
+ # Done
119
+ tui-eye stop
120
+ ```
121
+
122
+ ## Example: Multi-Agent Monitoring
123
+
124
+ ```bash
125
+ # Launch some test agents first
126
+ overcode launch --name test-agent-1 --prompt "Write hello world"
127
+ overcode launch --name test-agent-2 --prompt "List files"
128
+
129
+ # Start monitor
130
+ tui-eye start "overcode monitor" --size 220x45
131
+
132
+ # Periodic monitoring loop
133
+ tui-eye wait-for "test-agent" --timeout 30
134
+ tui-eye screenshot /tmp/monitor-1.png
135
+ # [Read - check agent statuses, timelines]
136
+
137
+ # If an agent needs attention, navigate and interact
138
+ tui-eye send j enter # Select agent
139
+ tui-eye screenshot /tmp/agent-detail.png
140
+ # [Read - see agent output]
141
+
142
+ # Continue monitoring...
143
+ tui-eye stop
144
+ ```
145
+
146
+ ## Visual Checks to Perform
147
+
148
+ When reading screenshots, check for:
149
+
150
+ - **Layout**: Header, timeline, agent list all visible?
151
+ - **Alignment**: Columns aligned, percentages right-justified?
152
+ - **Colors**: Status indicators using correct colors (green=running, red=waiting)?
153
+ - **Text**: No unexpected wrapping or truncation?
154
+ - **Timeline**: Bars extending full width? Percentage shown?
155
+ - **Responsiveness**: After interactions, UI updated correctly?
156
+
157
+ ## Troubleshooting
158
+
159
+ **Screenshot too narrow / lines wrapping:**
160
+ ```bash
161
+ tui-eye screenshot /tmp/x.png --width 220 --height 45
162
+ ```
163
+
164
+ **Can't see full content:**
165
+ Increase height:
166
+ ```bash
167
+ tui-eye start "overcode monitor" --size 220x60
168
+ ```
169
+
170
+ **Session already exists:**
171
+ ```bash
172
+ tui-eye stop
173
+ tui-eye start "overcode monitor"
174
+ ```
175
+
176
+ **Keys not working:**
177
+ Check session is running:
178
+ ```bash
179
+ tui-eye status
180
+ ```
181
+
182
+ ## Arguments
183
+
184
+ `$ARGUMENTS` - Optional test scenario to run. Examples:
185
+ - `help-toggle` - Test the help overlay toggle
186
+ - `navigation` - Test up/down navigation
187
+ - `full-smoke` - Run complete smoke test
overcode/tmux_manager.py CHANGED
@@ -1,11 +1,17 @@
1
1
  """
2
2
  Tmux session and window management for Overcode.
3
+
4
+ Uses libtmux for reliable tmux interaction.
3
5
  """
4
6
 
5
7
  import os
6
- import subprocess
8
+ import time
7
9
  from typing import Optional, List, Dict, Any, TYPE_CHECKING
8
10
 
11
+ import libtmux
12
+ from libtmux.exc import LibTmuxException
13
+ from libtmux._internal.query_list import ObjectDoesNotExist
14
+
9
15
  if TYPE_CHECKING:
10
16
  from .interfaces import TmuxInterface
11
17
 
@@ -13,7 +19,7 @@ if TYPE_CHECKING:
13
19
  class TmuxManager:
14
20
  """Manages tmux sessions and windows for Overcode.
15
21
 
16
- This class can be used directly (uses subprocess) or with an injected
22
+ This class can be used directly (uses libtmux) or with an injected
17
23
  TmuxInterface for testing.
18
24
  """
19
25
 
@@ -26,17 +32,44 @@ class TmuxManager:
26
32
  socket: Optional tmux socket name (for testing isolation)
27
33
  """
28
34
  self.session_name = session_name
29
- self._tmux = tmux # If None, use direct subprocess calls
35
+ self._tmux = tmux # If None, use libtmux directly
30
36
  # Support OVERCODE_TMUX_SOCKET env var for testing
31
37
  self.socket = socket or os.environ.get("OVERCODE_TMUX_SOCKET")
38
+ self._server: Optional[libtmux.Server] = None
39
+
40
+ @property
41
+ def server(self) -> libtmux.Server:
42
+ """Lazy-load the tmux server connection."""
43
+ if self._server is None:
44
+ if self.socket:
45
+ self._server = libtmux.Server(socket_name=self.socket)
46
+ else:
47
+ self._server = libtmux.Server()
48
+ return self._server
49
+
50
+ def _get_session(self) -> Optional[libtmux.Session]:
51
+ """Get the managed session, or None if it doesn't exist."""
52
+ try:
53
+ return self.server.sessions.get(session_name=self.session_name)
54
+ except (LibTmuxException, ObjectDoesNotExist):
55
+ return None
56
+
57
+ def _get_window(self, window_index: int) -> Optional[libtmux.Window]:
58
+ """Get a window by index."""
59
+ sess = self._get_session()
60
+ if sess is None:
61
+ return None
62
+ try:
63
+ return sess.windows.get(window_index=str(window_index))
64
+ except (LibTmuxException, ObjectDoesNotExist):
65
+ return None
32
66
 
33
- def _tmux_cmd(self, *args) -> List[str]:
34
- """Build tmux command with optional socket."""
35
- cmd = ["tmux"]
36
- if self.socket:
37
- cmd.extend(["-L", self.socket])
38
- cmd.extend(args)
39
- return cmd
67
+ def _get_pane(self, window_index: int) -> Optional[libtmux.Pane]:
68
+ """Get the first pane of a window."""
69
+ win = self._get_window(window_index)
70
+ if win is None or not win.panes:
71
+ return None
72
+ return win.panes[0]
40
73
 
41
74
  def ensure_session(self) -> bool:
42
75
  """Create tmux session if it doesn't exist"""
@@ -47,12 +80,9 @@ class TmuxManager:
47
80
  return self._tmux.new_session(self.session_name)
48
81
 
49
82
  try:
50
- subprocess.run(
51
- self._tmux_cmd("new-session", "-d", "-s", self.session_name),
52
- check=True
53
- )
83
+ self.server.new_session(session_name=self.session_name, attach=False)
54
84
  return True
55
- except subprocess.CalledProcessError:
85
+ except LibTmuxException:
56
86
  return False
57
87
 
58
88
  def session_exists(self) -> bool:
@@ -60,11 +90,10 @@ class TmuxManager:
60
90
  if self._tmux:
61
91
  return self._tmux.has_session(self.session_name)
62
92
 
63
- result = subprocess.run(
64
- self._tmux_cmd("has-session", "-t", self.session_name),
65
- capture_output=True
66
- )
67
- return result.returncode == 0
93
+ try:
94
+ return self.server.has_session(self.session_name)
95
+ except LibTmuxException:
96
+ return False
68
97
 
69
98
  def create_window(self, window_name: str, start_directory: Optional[str] = None) -> Optional[int]:
70
99
  """Create a new window in the tmux session"""
@@ -74,21 +103,18 @@ class TmuxManager:
74
103
  if self._tmux:
75
104
  return self._tmux.new_window(self.session_name, window_name, cwd=start_directory)
76
105
 
77
- args = [
78
- "new-window",
79
- "-t", self.session_name,
80
- "-n", window_name,
81
- "-P", # print window info
82
- "-F", "#{window_index}"
83
- ]
106
+ try:
107
+ sess = self._get_session()
108
+ if sess is None:
109
+ return None
84
110
 
85
- if start_directory:
86
- args.extend(["-c", start_directory])
111
+ kwargs: Dict[str, Any] = {'window_name': window_name, 'attach': False}
112
+ if start_directory:
113
+ kwargs['start_directory'] = start_directory
87
114
 
88
- try:
89
- result = subprocess.run(self._tmux_cmd(*args), capture_output=True, text=True, check=True)
90
- return int(result.stdout.strip())
91
- except (subprocess.CalledProcessError, ValueError):
115
+ window = sess.new_window(**kwargs)
116
+ return int(window.window_index)
117
+ except (LibTmuxException, ValueError):
92
118
  return None
93
119
 
94
120
  def send_keys(self, window_index: int, keys: str, enter: bool = True) -> bool:
@@ -97,31 +123,40 @@ class TmuxManager:
97
123
  For Claude Code: text and Enter must be sent as SEPARATE commands
98
124
  with a small delay, otherwise Claude Code doesn't process the Enter.
99
125
  """
100
- import time
101
-
102
126
  if self._tmux:
103
127
  return self._tmux.send_keys(self.session_name, window_index, keys, enter)
104
128
 
105
- target = f"{self.session_name}:{window_index}"
106
-
107
129
  try:
130
+ pane = self._get_pane(window_index)
131
+ if pane is None:
132
+ return False
133
+
108
134
  # Send text first (if any)
109
135
  if keys:
110
- subprocess.run(
111
- self._tmux_cmd("send-keys", "-t", target, keys),
112
- check=True
113
- )
114
- # Small delay for Claude Code to process text
115
- time.sleep(0.1)
136
+ # Special handling for ! commands (#139)
137
+ # Claude Code requires ! to be sent separately to trigger mode switch
138
+ # to bash mode before receiving the rest of the command
139
+ if keys.startswith('!') and len(keys) > 1:
140
+ # Send ! first
141
+ pane.send_keys('!', enter=False)
142
+ # Wait for mode switch to process
143
+ time.sleep(0.15)
144
+ # Send the rest (without the !)
145
+ rest = keys[1:]
146
+ if rest:
147
+ pane.send_keys(rest, enter=False)
148
+ time.sleep(0.1)
149
+ else:
150
+ pane.send_keys(keys, enter=False)
151
+ # Small delay for Claude Code to process text
152
+ time.sleep(0.1)
116
153
 
117
154
  # Send Enter separately
118
155
  if enter:
119
- subprocess.run(
120
- self._tmux_cmd("send-keys", "-t", target, "Enter"),
121
- check=True
122
- )
156
+ pane.send_keys('', enter=True)
157
+
123
158
  return True
124
- except subprocess.CalledProcessError:
159
+ except LibTmuxException:
125
160
  return False
126
161
 
127
162
  def attach_session(self):
@@ -129,7 +164,7 @@ class TmuxManager:
129
164
  if self._tmux:
130
165
  self._tmux.attach(self.session_name)
131
166
  return
132
- subprocess.run(self._tmux_cmd("attach", "-t", self.session_name))
167
+ os.execlp("tmux", "tmux", "attach-session", "-t", self.session_name)
133
168
 
134
169
  def list_windows(self) -> List[Dict[str, Any]]:
135
170
  """List all windows in the session.
@@ -148,31 +183,23 @@ class TmuxManager:
148
183
  ]
149
184
 
150
185
  try:
151
- result = subprocess.run(
152
- self._tmux_cmd(
153
- "list-windows",
154
- "-t", self.session_name,
155
- "-F", "#{window_index}|#{window_name}|#{pane_current_command}"
156
- ),
157
- capture_output=True, text=True, check=True
158
- )
186
+ sess = self._get_session()
187
+ if sess is None:
188
+ return []
159
189
 
160
190
  windows = []
161
- for line in result.stdout.strip().split("\n"):
162
- if line:
163
- parts = line.split("|")
164
- if len(parts) >= 3:
165
- try:
166
- window_index = int(parts[0])
167
- except ValueError:
168
- window_index = 0
169
- windows.append({
170
- "index": window_index,
171
- "name": parts[1],
172
- "command": parts[2]
173
- })
191
+ for win in sess.windows:
192
+ # Get command from first pane
193
+ command = ""
194
+ if win.panes:
195
+ command = win.panes[0].pane_current_command or ""
196
+ windows.append({
197
+ "index": int(win.window_index),
198
+ "name": win.window_name,
199
+ "command": command
200
+ })
174
201
  return windows
175
- except subprocess.CalledProcessError:
202
+ except LibTmuxException:
176
203
  return []
177
204
 
178
205
  def kill_window(self, window_index: int) -> bool:
@@ -181,12 +208,12 @@ class TmuxManager:
181
208
  return self._tmux.kill_window(self.session_name, window_index)
182
209
 
183
210
  try:
184
- subprocess.run(
185
- self._tmux_cmd("kill-window", "-t", f"{self.session_name}:{window_index}"),
186
- check=True
187
- )
211
+ win = self._get_window(window_index)
212
+ if win is None:
213
+ return False
214
+ win.kill()
188
215
  return True
189
- except subprocess.CalledProcessError:
216
+ except LibTmuxException:
190
217
  return False
191
218
 
192
219
  def kill_session(self) -> bool:
@@ -195,12 +222,12 @@ class TmuxManager:
195
222
  return self._tmux.kill_session(self.session_name)
196
223
 
197
224
  try:
198
- subprocess.run(
199
- self._tmux_cmd("kill-session", "-t", self.session_name),
200
- check=True
201
- )
225
+ sess = self._get_session()
226
+ if sess is None:
227
+ return False
228
+ sess.kill()
202
229
  return True
203
- except subprocess.CalledProcessError:
230
+ except LibTmuxException:
204
231
  return False
205
232
 
206
233
  def window_exists(self, window_index: int) -> bool:
@@ -213,16 +240,13 @@ class TmuxManager:
213
240
  return any(w.get('index') == window_index for w in windows)
214
241
 
215
242
  try:
216
- result = subprocess.run(
217
- self._tmux_cmd(
218
- "list-windows",
219
- "-t", self.session_name,
220
- "-F", "#{window_index}"
221
- ),
222
- capture_output=True, text=True, check=True
223
- )
224
-
225
- window_indices = [int(idx.strip()) for idx in result.stdout.strip().split("\n") if idx.strip()]
226
- return window_index in window_indices
227
- except (subprocess.CalledProcessError, ValueError):
243
+ sess = self._get_session()
244
+ if sess is None:
245
+ return False
246
+
247
+ for win in sess.windows:
248
+ if int(win.window_index) == window_index:
249
+ return True
250
+ return False
251
+ except LibTmuxException:
228
252
  return False