overcode 0.1.3__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 (41) hide show
  1. overcode/__init__.py +1 -1
  2. overcode/cli.py +7 -2
  3. overcode/implementations.py +74 -8
  4. overcode/monitor_daemon.py +60 -65
  5. overcode/monitor_daemon_core.py +261 -0
  6. overcode/monitor_daemon_state.py +7 -0
  7. overcode/session_manager.py +1 -0
  8. overcode/settings.py +22 -0
  9. overcode/supervisor_daemon.py +48 -47
  10. overcode/supervisor_daemon_core.py +210 -0
  11. overcode/testing/__init__.py +6 -0
  12. overcode/testing/renderer.py +268 -0
  13. overcode/testing/tmux_driver.py +223 -0
  14. overcode/testing/tui_eye.py +185 -0
  15. overcode/testing/tui_eye_skill.md +187 -0
  16. overcode/tmux_manager.py +17 -3
  17. overcode/tui.py +196 -2462
  18. overcode/tui_actions/__init__.py +20 -0
  19. overcode/tui_actions/daemon.py +201 -0
  20. overcode/tui_actions/input.py +128 -0
  21. overcode/tui_actions/navigation.py +117 -0
  22. overcode/tui_actions/session.py +428 -0
  23. overcode/tui_actions/view.py +357 -0
  24. overcode/tui_helpers.py +41 -9
  25. overcode/tui_logic.py +347 -0
  26. overcode/tui_render.py +414 -0
  27. overcode/tui_widgets/__init__.py +24 -0
  28. overcode/tui_widgets/command_bar.py +399 -0
  29. overcode/tui_widgets/daemon_panel.py +153 -0
  30. overcode/tui_widgets/daemon_status_bar.py +245 -0
  31. overcode/tui_widgets/help_overlay.py +71 -0
  32. overcode/tui_widgets/preview_pane.py +69 -0
  33. overcode/tui_widgets/session_summary.py +514 -0
  34. overcode/tui_widgets/status_timeline.py +253 -0
  35. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/METADATA +3 -1
  36. overcode-0.1.4.dist-info/RECORD +68 -0
  37. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
  38. overcode-0.1.3.dist-info/RECORD +0 -45
  39. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/WHEEL +0 -0
  40. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
  41. {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,223 @@
1
+ """tmux session management for TUI testing."""
2
+
3
+ import time
4
+ from typing import Optional
5
+ import libtmux
6
+
7
+
8
+ class TUIDriver:
9
+ """Manages a tmux session for testing TUI applications.
10
+
11
+ This class provides a simple interface to:
12
+ - Start a TUI app in a tmux session with controlled dimensions
13
+ - Send keystrokes to the app
14
+ - Capture the screen content (with ANSI codes)
15
+ - Wait for specific content to appear
16
+ - Clean up the session
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ session_name: str = "tui-eye",
22
+ socket_name: Optional[str] = None,
23
+ ):
24
+ """Initialize the TUI driver.
25
+
26
+ Args:
27
+ session_name: Name for the tmux session
28
+ socket_name: Optional tmux socket name (for isolation)
29
+ """
30
+ self.session_name = session_name
31
+ self.socket_name = socket_name
32
+ self.server: Optional[libtmux.Server] = None
33
+ self.session: Optional[libtmux.Session] = None
34
+
35
+ def start(
36
+ self,
37
+ command: str,
38
+ width: int = 120,
39
+ height: int = 40,
40
+ env: Optional[dict[str, str]] = None,
41
+ ) -> None:
42
+ """Start a TUI application in a new tmux session.
43
+
44
+ Args:
45
+ command: The command to run (e.g., "overcode supervisor")
46
+ width: Terminal width in characters
47
+ height: Terminal height in characters
48
+ env: Optional environment variables to set
49
+ """
50
+ # Kill any existing session with this name
51
+ self.stop()
52
+
53
+ # Create server connection
54
+ if self.socket_name:
55
+ self.server = libtmux.Server(socket_name=self.socket_name)
56
+ else:
57
+ self.server = libtmux.Server()
58
+
59
+ # Create new session
60
+ self.session = self.server.new_session(
61
+ session_name=self.session_name,
62
+ window_command=command,
63
+ x=width,
64
+ y=height,
65
+ environment=env,
66
+ )
67
+
68
+ def send_keys(self, *keys: str, enter: bool = False) -> None:
69
+ """Send keystrokes to the TUI.
70
+
71
+ Args:
72
+ keys: Key names to send (e.g., "j", "k", "Enter", "escape")
73
+ enter: Whether to send Enter after all keys
74
+ """
75
+ if not self.session:
76
+ self.connect()
77
+ if not self.session:
78
+ raise RuntimeError("No active session. Call start() first.")
79
+
80
+ pane = self.session.active_window.active_pane
81
+
82
+ for key in keys:
83
+ # Map common key names
84
+ key_map = {
85
+ "enter": "Enter",
86
+ "escape": "Escape",
87
+ "esc": "Escape",
88
+ "tab": "Tab",
89
+ "space": "Space",
90
+ "up": "Up",
91
+ "down": "Down",
92
+ "left": "Left",
93
+ "right": "Right",
94
+ "backspace": "BSpace",
95
+ }
96
+ mapped_key = key_map.get(key.lower(), key)
97
+ pane.send_keys(mapped_key, enter=False)
98
+
99
+ if enter:
100
+ pane.send_keys("Enter", enter=False)
101
+
102
+ def capture(self, with_ansi: bool = True) -> str:
103
+ """Capture the current screen content.
104
+
105
+ Args:
106
+ with_ansi: Whether to include ANSI escape codes
107
+
108
+ Returns:
109
+ The screen content as a string
110
+ """
111
+ if not self.session:
112
+ self.connect()
113
+ if not self.session:
114
+ raise RuntimeError("No active session. Call start() first.")
115
+
116
+ pane = self.session.active_window.active_pane
117
+
118
+ # Use capture_pane with escape sequences if requested
119
+ if with_ansi:
120
+ # -e preserves ANSI escape sequences
121
+ # -p prints to stdout
122
+ content = pane.cmd("capture-pane", "-e", "-p").stdout
123
+ else:
124
+ content = pane.cmd("capture-pane", "-p").stdout
125
+
126
+ return "\n".join(content) if isinstance(content, list) else content
127
+
128
+ def wait_for(
129
+ self,
130
+ text: str,
131
+ timeout: float = 10.0,
132
+ poll_interval: float = 0.2,
133
+ ) -> bool:
134
+ """Wait for specific text to appear on screen.
135
+
136
+ Args:
137
+ text: The text to wait for
138
+ timeout: Maximum time to wait in seconds
139
+ poll_interval: Time between checks in seconds
140
+
141
+ Returns:
142
+ True if text was found, False if timeout occurred
143
+ """
144
+ start_time = time.time()
145
+
146
+ while time.time() - start_time < timeout:
147
+ content = self.capture(with_ansi=False)
148
+ if text in content:
149
+ return True
150
+ time.sleep(poll_interval)
151
+
152
+ return False
153
+
154
+ def stop(self) -> None:
155
+ """Stop and clean up the tmux session."""
156
+ if self.session:
157
+ try:
158
+ self.session.kill()
159
+ except Exception:
160
+ pass
161
+ self.session = None
162
+
163
+ # Always try to kill by name, connecting first if needed
164
+ try:
165
+ if not self.server:
166
+ if self.socket_name:
167
+ self.server = libtmux.Server(socket_name=self.socket_name)
168
+ else:
169
+ self.server = libtmux.Server()
170
+
171
+ for session in self.server.sessions:
172
+ if session.name == self.session_name:
173
+ session.kill()
174
+ break
175
+ except Exception:
176
+ pass
177
+
178
+ def __enter__(self) -> "TUIDriver":
179
+ """Context manager entry."""
180
+ return self
181
+
182
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
183
+ """Context manager exit - ensures cleanup."""
184
+ self.stop()
185
+
186
+ def connect(self) -> bool:
187
+ """Connect to an existing tmux session.
188
+
189
+ Returns:
190
+ True if successfully connected, False if session doesn't exist
191
+ """
192
+ if self.socket_name:
193
+ self.server = libtmux.Server(socket_name=self.socket_name)
194
+ else:
195
+ self.server = libtmux.Server()
196
+
197
+ try:
198
+ for session in self.server.sessions:
199
+ if session.name == self.session_name:
200
+ self.session = session
201
+ return True
202
+ except Exception:
203
+ pass
204
+
205
+ return False
206
+
207
+ @property
208
+ def is_running(self) -> bool:
209
+ """Check if the session is still running."""
210
+ # First try to connect if we don't have a session reference
211
+ if not self.session:
212
+ self.connect()
213
+
214
+ if not self.session:
215
+ return False
216
+
217
+ try:
218
+ # Try to access the session to verify it exists
219
+ _ = self.session.name
220
+ return True
221
+ except Exception:
222
+ self.session = None
223
+ return False
@@ -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
@@ -133,9 +133,23 @@ class TmuxManager:
133
133
 
134
134
  # Send text first (if any)
135
135
  if keys:
136
- pane.send_keys(keys, enter=False)
137
- # Small delay for Claude Code to process text
138
- 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)
139
153
 
140
154
  # Send Enter separately
141
155
  if enter: