overcode 0.1.2__py3-none-any.whl → 0.1.3__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 +147 -49
- overcode/config.py +66 -0
- overcode/daemon_claude_skill.md +36 -33
- overcode/history_reader.py +69 -8
- overcode/implementations.py +109 -84
- overcode/monitor_daemon.py +33 -38
- overcode/monitor_daemon_state.py +17 -15
- overcode/pid_utils.py +17 -3
- overcode/session_manager.py +53 -0
- overcode/settings.py +12 -0
- overcode/status_constants.py +1 -1
- overcode/status_detector.py +8 -2
- overcode/status_patterns.py +19 -0
- overcode/summarizer_client.py +72 -27
- overcode/summarizer_component.py +87 -107
- overcode/supervisor_daemon.py +21 -5
- overcode/tmux_manager.py +101 -91
- overcode/tui.py +829 -133
- overcode/tui_helpers.py +4 -3
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/METADATA +2 -1
- overcode-0.1.3.dist-info/RECORD +45 -0
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/WHEEL +1 -1
- overcode-0.1.2.dist-info/RECORD +0 -45
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/entry_points.txt +0 -0
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/top_level.txt +0 -0
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
|
|
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
|
|
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
|
|
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
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
if
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
self.
|
|
65
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
111
|
+
kwargs: Dict[str, Any] = {'window_name': window_name, 'attach': False}
|
|
112
|
+
if start_directory:
|
|
113
|
+
kwargs['start_directory'] = start_directory
|
|
87
114
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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,26 @@ 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
|
-
|
|
111
|
-
self._tmux_cmd("send-keys", "-t", target, keys),
|
|
112
|
-
check=True
|
|
113
|
-
)
|
|
136
|
+
pane.send_keys(keys, enter=False)
|
|
114
137
|
# Small delay for Claude Code to process text
|
|
115
138
|
time.sleep(0.1)
|
|
116
139
|
|
|
117
140
|
# Send Enter separately
|
|
118
141
|
if enter:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
check=True
|
|
122
|
-
)
|
|
142
|
+
pane.send_keys('', enter=True)
|
|
143
|
+
|
|
123
144
|
return True
|
|
124
|
-
except
|
|
145
|
+
except LibTmuxException:
|
|
125
146
|
return False
|
|
126
147
|
|
|
127
148
|
def attach_session(self):
|
|
@@ -129,7 +150,7 @@ class TmuxManager:
|
|
|
129
150
|
if self._tmux:
|
|
130
151
|
self._tmux.attach(self.session_name)
|
|
131
152
|
return
|
|
132
|
-
|
|
153
|
+
os.execlp("tmux", "tmux", "attach-session", "-t", self.session_name)
|
|
133
154
|
|
|
134
155
|
def list_windows(self) -> List[Dict[str, Any]]:
|
|
135
156
|
"""List all windows in the session.
|
|
@@ -148,31 +169,23 @@ class TmuxManager:
|
|
|
148
169
|
]
|
|
149
170
|
|
|
150
171
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"-t", self.session_name,
|
|
155
|
-
"-F", "#{window_index}|#{window_name}|#{pane_current_command}"
|
|
156
|
-
),
|
|
157
|
-
capture_output=True, text=True, check=True
|
|
158
|
-
)
|
|
172
|
+
sess = self._get_session()
|
|
173
|
+
if sess is None:
|
|
174
|
+
return []
|
|
159
175
|
|
|
160
176
|
windows = []
|
|
161
|
-
for
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"name": parts[1],
|
|
172
|
-
"command": parts[2]
|
|
173
|
-
})
|
|
177
|
+
for win in sess.windows:
|
|
178
|
+
# Get command from first pane
|
|
179
|
+
command = ""
|
|
180
|
+
if win.panes:
|
|
181
|
+
command = win.panes[0].pane_current_command or ""
|
|
182
|
+
windows.append({
|
|
183
|
+
"index": int(win.window_index),
|
|
184
|
+
"name": win.window_name,
|
|
185
|
+
"command": command
|
|
186
|
+
})
|
|
174
187
|
return windows
|
|
175
|
-
except
|
|
188
|
+
except LibTmuxException:
|
|
176
189
|
return []
|
|
177
190
|
|
|
178
191
|
def kill_window(self, window_index: int) -> bool:
|
|
@@ -181,12 +194,12 @@ class TmuxManager:
|
|
|
181
194
|
return self._tmux.kill_window(self.session_name, window_index)
|
|
182
195
|
|
|
183
196
|
try:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
197
|
+
win = self._get_window(window_index)
|
|
198
|
+
if win is None:
|
|
199
|
+
return False
|
|
200
|
+
win.kill()
|
|
188
201
|
return True
|
|
189
|
-
except
|
|
202
|
+
except LibTmuxException:
|
|
190
203
|
return False
|
|
191
204
|
|
|
192
205
|
def kill_session(self) -> bool:
|
|
@@ -195,12 +208,12 @@ class TmuxManager:
|
|
|
195
208
|
return self._tmux.kill_session(self.session_name)
|
|
196
209
|
|
|
197
210
|
try:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
211
|
+
sess = self._get_session()
|
|
212
|
+
if sess is None:
|
|
213
|
+
return False
|
|
214
|
+
sess.kill()
|
|
202
215
|
return True
|
|
203
|
-
except
|
|
216
|
+
except LibTmuxException:
|
|
204
217
|
return False
|
|
205
218
|
|
|
206
219
|
def window_exists(self, window_index: int) -> bool:
|
|
@@ -213,16 +226,13 @@ class TmuxManager:
|
|
|
213
226
|
return any(w.get('index') == window_index for w in windows)
|
|
214
227
|
|
|
215
228
|
try:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
|
|
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):
|
|
229
|
+
sess = self._get_session()
|
|
230
|
+
if sess is None:
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
for win in sess.windows:
|
|
234
|
+
if int(win.window_index) == window_index:
|
|
235
|
+
return True
|
|
236
|
+
return False
|
|
237
|
+
except LibTmuxException:
|
|
228
238
|
return False
|