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.
- overcode/__init__.py +1 -1
- overcode/cli.py +154 -51
- overcode/config.py +66 -0
- overcode/daemon_claude_skill.md +36 -33
- overcode/history_reader.py +69 -8
- overcode/implementations.py +178 -87
- overcode/monitor_daemon.py +87 -97
- overcode/monitor_daemon_core.py +261 -0
- overcode/monitor_daemon_state.py +24 -15
- overcode/pid_utils.py +17 -3
- overcode/session_manager.py +54 -0
- overcode/settings.py +34 -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 +55 -38
- 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 +117 -93
- overcode/tui.py +399 -1969
- 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 +42 -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.2.dist-info → overcode-0.1.4.dist-info}/METADATA +4 -1
- overcode-0.1.4.dist-info/RECORD +68 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/WHEEL +1 -1
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
- overcode-0.1.2.dist-info/RECORD +0 -45
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
overcode/tui_logic.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pure business logic functions for TUI components.
|
|
3
|
+
|
|
4
|
+
These functions are extracted from the TUI to enable unit testing
|
|
5
|
+
without requiring the full Textual framework or actual session objects.
|
|
6
|
+
|
|
7
|
+
All functions are pure - they take data as input and return new data.
|
|
8
|
+
No side effects, no mutations of input data.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import List, Set, Optional, TypeVar, Protocol, Tuple
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SessionLike(Protocol):
|
|
17
|
+
"""Protocol for session-like objects used in sorting/filtering."""
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str: ...
|
|
20
|
+
@property
|
|
21
|
+
def id(self) -> str: ...
|
|
22
|
+
@property
|
|
23
|
+
def is_asleep(self) -> bool: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SessionWithStats(SessionLike, Protocol):
|
|
27
|
+
"""Protocol for sessions with stats for sorting."""
|
|
28
|
+
@property
|
|
29
|
+
def stats(self) -> "StatsLike": ...
|
|
30
|
+
@property
|
|
31
|
+
def agent_value(self) -> float: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StatsLike(Protocol):
|
|
35
|
+
"""Protocol for stats-like objects."""
|
|
36
|
+
@property
|
|
37
|
+
def current_state(self) -> Optional[str]: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
T = TypeVar('T', bound=SessionLike)
|
|
41
|
+
S = TypeVar('S', bound=SessionWithStats)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Status priority orders for sorting
|
|
45
|
+
STATUS_ORDER_BY_ATTENTION = {
|
|
46
|
+
"waiting_user": 0,
|
|
47
|
+
"waiting_supervisor": 1,
|
|
48
|
+
"no_instructions": 2,
|
|
49
|
+
"error": 3,
|
|
50
|
+
"running": 4,
|
|
51
|
+
"terminated": 5,
|
|
52
|
+
"asleep": 6,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
STATUS_ORDER_BY_VALUE = {
|
|
56
|
+
"waiting_user": 0,
|
|
57
|
+
"waiting_supervisor": 0,
|
|
58
|
+
"no_instructions": 0,
|
|
59
|
+
"error": 0,
|
|
60
|
+
"running": 1,
|
|
61
|
+
"terminated": 2,
|
|
62
|
+
"asleep": 2,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def sort_sessions_alphabetical(sessions: List[T]) -> List[T]:
|
|
67
|
+
"""Sort sessions alphabetically by name (case-insensitive).
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
sessions: List of session objects with a name attribute
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
New sorted list (does not mutate input)
|
|
74
|
+
"""
|
|
75
|
+
return sorted(sessions, key=lambda s: s.name.lower())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def sort_sessions_by_status(sessions: List[S]) -> List[S]:
|
|
79
|
+
"""Sort sessions by status priority, then alphabetically.
|
|
80
|
+
|
|
81
|
+
Priority order: waiting_user, waiting_supervisor, no_instructions,
|
|
82
|
+
error, running, terminated, asleep.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
sessions: List of session objects with stats.current_state
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
New sorted list (does not mutate input)
|
|
89
|
+
"""
|
|
90
|
+
return sorted(
|
|
91
|
+
sessions,
|
|
92
|
+
key=lambda s: (
|
|
93
|
+
STATUS_ORDER_BY_ATTENTION.get(s.stats.current_state or "running", 4),
|
|
94
|
+
s.name.lower()
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def sort_sessions_by_value(sessions: List[S]) -> List[S]:
|
|
100
|
+
"""Sort sessions by value (priority) descending, then alphabetically.
|
|
101
|
+
|
|
102
|
+
Non-green agents (needing attention) sort first, then by agent_value
|
|
103
|
+
descending within each group.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
sessions: List of session objects with stats.current_state and agent_value
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
New sorted list (does not mutate input)
|
|
110
|
+
"""
|
|
111
|
+
return sorted(
|
|
112
|
+
sessions,
|
|
113
|
+
key=lambda s: (
|
|
114
|
+
STATUS_ORDER_BY_VALUE.get(s.stats.current_state or "running", 1),
|
|
115
|
+
-s.agent_value,
|
|
116
|
+
s.name.lower()
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def sort_sessions(sessions: List[S], mode: str) -> List[S]:
|
|
122
|
+
"""Sort sessions based on the specified mode.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
sessions: List of session objects
|
|
126
|
+
mode: One of "alphabetical", "by_status", "by_value"
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
New sorted list (does not mutate input)
|
|
130
|
+
"""
|
|
131
|
+
if mode == "alphabetical":
|
|
132
|
+
return sort_sessions_alphabetical(sessions)
|
|
133
|
+
elif mode == "by_status":
|
|
134
|
+
return sort_sessions_by_status(sessions)
|
|
135
|
+
elif mode == "by_value":
|
|
136
|
+
return sort_sessions_by_value(sessions)
|
|
137
|
+
else:
|
|
138
|
+
# Default to alphabetical for unknown modes
|
|
139
|
+
return sort_sessions_alphabetical(sessions)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def filter_visible_sessions(
|
|
143
|
+
active_sessions: List[T],
|
|
144
|
+
terminated_sessions: List[T],
|
|
145
|
+
hide_asleep: bool,
|
|
146
|
+
show_terminated: bool,
|
|
147
|
+
) -> List[T]:
|
|
148
|
+
"""Filter sessions based on visibility preferences.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
active_sessions: List of currently active sessions
|
|
152
|
+
terminated_sessions: List of terminated/killed sessions
|
|
153
|
+
hide_asleep: If True, filter out sleeping agents
|
|
154
|
+
show_terminated: If True, include terminated sessions
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
New filtered list (does not mutate inputs)
|
|
158
|
+
"""
|
|
159
|
+
result = list(active_sessions)
|
|
160
|
+
|
|
161
|
+
# Filter out sleeping agents if requested
|
|
162
|
+
if hide_asleep:
|
|
163
|
+
result = [s for s in result if not s.is_asleep]
|
|
164
|
+
|
|
165
|
+
# Include terminated sessions if requested
|
|
166
|
+
if show_terminated:
|
|
167
|
+
active_ids = {s.id for s in active_sessions}
|
|
168
|
+
for session in terminated_sessions:
|
|
169
|
+
if session.id not in active_ids:
|
|
170
|
+
result.append(session)
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_sort_mode_display_name(mode: str) -> str:
|
|
176
|
+
"""Get human-readable display name for sort mode.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
mode: Sort mode identifier
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Human-readable name
|
|
183
|
+
"""
|
|
184
|
+
mode_names = {
|
|
185
|
+
"alphabetical": "Alphabetical",
|
|
186
|
+
"by_status": "By Status",
|
|
187
|
+
"by_value": "By Value (priority)",
|
|
188
|
+
}
|
|
189
|
+
return mode_names.get(mode, mode)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cycle_sort_mode(current_mode: str, available_modes: List[str]) -> str:
|
|
193
|
+
"""Get the next sort mode in the cycle.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
current_mode: Current sort mode
|
|
197
|
+
available_modes: List of available sort modes
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Next sort mode in the cycle
|
|
201
|
+
"""
|
|
202
|
+
if not available_modes:
|
|
203
|
+
return current_mode
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
current_idx = available_modes.index(current_mode)
|
|
207
|
+
except ValueError:
|
|
208
|
+
current_idx = -1
|
|
209
|
+
|
|
210
|
+
new_idx = (current_idx + 1) % len(available_modes)
|
|
211
|
+
return available_modes[new_idx]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class SpinStats:
|
|
216
|
+
"""Statistics for spin rate display."""
|
|
217
|
+
green_count: int
|
|
218
|
+
total_count: int
|
|
219
|
+
sleeping_count: int
|
|
220
|
+
mean_spin: float
|
|
221
|
+
total_tokens: int
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def calculate_spin_stats(
|
|
225
|
+
sessions: List,
|
|
226
|
+
asleep_session_ids: Set[str],
|
|
227
|
+
) -> SpinStats:
|
|
228
|
+
"""Calculate spin rate statistics from sessions.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
sessions: List of session daemon states with green_time_seconds,
|
|
232
|
+
non_green_time_seconds, current_status, input_tokens, output_tokens
|
|
233
|
+
asleep_session_ids: Set of session IDs that are asleep
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
SpinStats dataclass with calculated values
|
|
237
|
+
"""
|
|
238
|
+
# Filter out sleeping agents for active stats
|
|
239
|
+
active_sessions = [s for s in sessions if s.session_id not in asleep_session_ids]
|
|
240
|
+
sleeping_count = len(sessions) - len(active_sessions)
|
|
241
|
+
|
|
242
|
+
total_count = len(active_sessions)
|
|
243
|
+
green_count = sum(1 for s in active_sessions if s.current_status == "running")
|
|
244
|
+
|
|
245
|
+
# Calculate mean spin rate
|
|
246
|
+
mean_spin = 0.0
|
|
247
|
+
for s in active_sessions:
|
|
248
|
+
total_time = s.green_time_seconds + s.non_green_time_seconds
|
|
249
|
+
if total_time > 0:
|
|
250
|
+
mean_spin += s.green_time_seconds / total_time
|
|
251
|
+
|
|
252
|
+
# Total tokens (include sleeping agents)
|
|
253
|
+
total_tokens = sum(s.input_tokens + s.output_tokens for s in sessions)
|
|
254
|
+
|
|
255
|
+
return SpinStats(
|
|
256
|
+
green_count=green_count,
|
|
257
|
+
total_count=total_count,
|
|
258
|
+
sleeping_count=sleeping_count,
|
|
259
|
+
mean_spin=mean_spin,
|
|
260
|
+
total_tokens=total_tokens,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def calculate_mean_spin_from_history(
|
|
265
|
+
history: List[Tuple[datetime, str, str, str]],
|
|
266
|
+
agent_names: List[str],
|
|
267
|
+
baseline_minutes: int,
|
|
268
|
+
now: Optional[datetime] = None,
|
|
269
|
+
) -> Tuple[float, int]:
|
|
270
|
+
"""Calculate mean spin rate from CSV history within a time window.
|
|
271
|
+
|
|
272
|
+
This provides a time-windowed average of how many agents were running,
|
|
273
|
+
as opposed to the cumulative calculation in calculate_spin_stats().
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
history: List of (timestamp, agent, status, activity) tuples from CSV
|
|
277
|
+
agent_names: List of active (non-sleeping) agent names to include
|
|
278
|
+
baseline_minutes: Minutes back from now (0 = instantaneous, not used)
|
|
279
|
+
now: Reference time (defaults to datetime.now())
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Tuple of (mean_spin, sample_count) where:
|
|
283
|
+
- mean_spin: Average number of agents in "running" state during window
|
|
284
|
+
- sample_count: Total samples in the window (0 if no data)
|
|
285
|
+
"""
|
|
286
|
+
if now is None:
|
|
287
|
+
now = datetime.now()
|
|
288
|
+
|
|
289
|
+
if baseline_minutes <= 0 or not agent_names:
|
|
290
|
+
return (0.0, 0)
|
|
291
|
+
|
|
292
|
+
cutoff = now - timedelta(minutes=baseline_minutes)
|
|
293
|
+
|
|
294
|
+
# Filter to window and active agents only
|
|
295
|
+
window_history = [
|
|
296
|
+
(ts, agent, status)
|
|
297
|
+
for ts, agent, status, _ in history
|
|
298
|
+
if cutoff <= ts <= now and agent in agent_names
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
if not window_history:
|
|
302
|
+
return (0.0, 0)
|
|
303
|
+
|
|
304
|
+
running_count = sum(1 for _, _, status in window_history if status == "running")
|
|
305
|
+
total_count = len(window_history)
|
|
306
|
+
|
|
307
|
+
# mean_spin = (fraction of samples that were "running") * num_agents
|
|
308
|
+
# This gives "average number of agents running at any point in time"
|
|
309
|
+
# Example: 2 agents, 50% of samples are "running" -> mean_spin = 1.0
|
|
310
|
+
num_agents = len(agent_names)
|
|
311
|
+
mean_spin = (running_count / total_count) * num_agents if total_count > 0 else 0.0
|
|
312
|
+
|
|
313
|
+
return (mean_spin, total_count)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def calculate_green_percentage(green_time: float, non_green_time: float) -> float:
|
|
317
|
+
"""Calculate the percentage of time spent in green (running) state.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
green_time: Total green time in seconds
|
|
321
|
+
non_green_time: Total non-green time in seconds
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Percentage (0-100) of time in green state
|
|
325
|
+
"""
|
|
326
|
+
total_time = green_time + non_green_time
|
|
327
|
+
if total_time <= 0:
|
|
328
|
+
return 0.0
|
|
329
|
+
return green_time / total_time * 100
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def calculate_human_interaction_count(
|
|
333
|
+
total_interactions: Optional[int],
|
|
334
|
+
robot_interactions: int,
|
|
335
|
+
) -> int:
|
|
336
|
+
"""Calculate number of human interactions.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
total_interactions: Total interaction count (or None)
|
|
340
|
+
robot_interactions: Number of robot/supervisor interactions
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Number of human interactions (clamped to 0 minimum)
|
|
344
|
+
"""
|
|
345
|
+
if total_interactions is None:
|
|
346
|
+
return 0
|
|
347
|
+
return max(0, total_interactions - robot_interactions)
|