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.
- overcode/__init__.py +1 -1
- overcode/cli.py +7 -2
- overcode/implementations.py +74 -8
- overcode/monitor_daemon.py +60 -65
- overcode/monitor_daemon_core.py +261 -0
- overcode/monitor_daemon_state.py +7 -0
- overcode/session_manager.py +1 -0
- overcode/settings.py +22 -0
- overcode/supervisor_daemon.py +48 -47
- overcode/supervisor_daemon_core.py +210 -0
- overcode/testing/__init__.py +6 -0
- overcode/testing/renderer.py +268 -0
- overcode/testing/tmux_driver.py +223 -0
- overcode/testing/tui_eye.py +185 -0
- overcode/testing/tui_eye_skill.md +187 -0
- overcode/tmux_manager.py +17 -3
- overcode/tui.py +196 -2462
- overcode/tui_actions/__init__.py +20 -0
- overcode/tui_actions/daemon.py +201 -0
- overcode/tui_actions/input.py +128 -0
- overcode/tui_actions/navigation.py +117 -0
- overcode/tui_actions/session.py +428 -0
- overcode/tui_actions/view.py +357 -0
- overcode/tui_helpers.py +41 -9
- overcode/tui_logic.py +347 -0
- overcode/tui_render.py +414 -0
- overcode/tui_widgets/__init__.py +24 -0
- overcode/tui_widgets/command_bar.py +399 -0
- overcode/tui_widgets/daemon_panel.py +153 -0
- overcode/tui_widgets/daemon_status_bar.py +245 -0
- overcode/tui_widgets/help_overlay.py +71 -0
- overcode/tui_widgets/preview_pane.py +69 -0
- overcode/tui_widgets/session_summary.py +514 -0
- overcode/tui_widgets/status_timeline.py +253 -0
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/METADATA +3 -1
- overcode-0.1.4.dist-info/RECORD +68 -0
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
- overcode-0.1.3.dist-info/RECORD +0 -45
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/WHEEL +0 -0
- {overcode-0.1.3.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
137
|
-
#
|
|
138
|
-
|
|
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:
|