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,245 @@
1
+ """
2
+ Daemon status bar widget for TUI.
3
+
4
+ Shows Monitor Daemon, Supervisor Daemon, AI, spin stats, and presence status.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Optional, TYPE_CHECKING
9
+
10
+ from textual.widgets import Static
11
+ from rich.text import Text
12
+
13
+ from ..monitor_daemon_state import MonitorDaemonState, get_monitor_daemon_state
14
+ from ..supervisor_daemon import is_supervisor_daemon_running
15
+ from ..summarizer_client import SummarizerClient
16
+ from ..web_server import is_web_server_running, get_web_server_url
17
+ from ..settings import DAEMON_VERSION, get_agent_history_path
18
+ from ..status_history import read_agent_status_history
19
+ from ..tui_logic import calculate_mean_spin_from_history
20
+ from ..tui_helpers import (
21
+ format_interval,
22
+ format_duration,
23
+ format_tokens,
24
+ format_cost,
25
+ get_daemon_status_style,
26
+ calculate_safe_break_duration,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from ..session_manager import SessionManager
31
+
32
+
33
+ class DaemonStatusBar(Static):
34
+ """Widget displaying daemon status.
35
+
36
+ Shows Monitor Daemon and Supervisor Daemon status explicitly.
37
+ Presence is shown only when available (macOS with monitor daemon running).
38
+ """
39
+
40
+ def __init__(self, tmux_session: str = "agents", session_manager: Optional["SessionManager"] = None, *args, **kwargs):
41
+ super().__init__(*args, **kwargs)
42
+ self.tmux_session = tmux_session
43
+ self.monitor_state: Optional[MonitorDaemonState] = None
44
+ self._session_manager = session_manager
45
+ self._asleep_session_ids: set = set() # Cache of asleep session IDs
46
+ self.show_cost: bool = False # Show $ cost instead of token counts
47
+
48
+ def update_status(self) -> None:
49
+ """Refresh daemon state from file"""
50
+ self.monitor_state = get_monitor_daemon_state(self.tmux_session)
51
+ # Update cache of asleep session IDs from session manager
52
+ if self._session_manager:
53
+ self._asleep_session_ids = {
54
+ s.id for s in self._session_manager.list_sessions() if s.is_asleep
55
+ }
56
+ self.refresh()
57
+
58
+ def render(self) -> Text:
59
+ """Render daemon status bar.
60
+
61
+ Shows Monitor Daemon and Supervisor Daemon status explicitly.
62
+ """
63
+ content = Text()
64
+
65
+ # Monitor Daemon status
66
+ content.append("Monitor: ", style="bold")
67
+ monitor_running = self.monitor_state and not self.monitor_state.is_stale()
68
+
69
+ if monitor_running:
70
+ state = self.monitor_state
71
+ symbol, style = get_daemon_status_style(state.status)
72
+ content.append(f"{symbol} ", style=style)
73
+ content.append(f"#{state.loop_count}", style="cyan")
74
+ content.append(f" @{format_interval(state.current_interval)}", style="dim")
75
+ # Version mismatch warning
76
+ if state.daemon_version != DAEMON_VERSION:
77
+ content.append(f" ⚠v{state.daemon_version}→{DAEMON_VERSION}", style="bold yellow")
78
+ else:
79
+ content.append("○ ", style="red")
80
+ content.append("stopped", style="red")
81
+
82
+ content.append(" │ ", style="dim")
83
+
84
+ # Supervisor Daemon status
85
+ content.append("Supervisor: ", style="bold")
86
+ supervisor_running = is_supervisor_daemon_running(self.tmux_session)
87
+
88
+ if supervisor_running:
89
+ content.append("● ", style="green")
90
+ # Show if daemon Claude is currently running
91
+ if monitor_running and self.monitor_state.supervisor_claude_running:
92
+ # Calculate current run duration
93
+ run_duration = ""
94
+ if self.monitor_state.supervisor_claude_started_at:
95
+ try:
96
+ started = datetime.fromisoformat(self.monitor_state.supervisor_claude_started_at)
97
+ elapsed = (datetime.now() - started).total_seconds()
98
+ run_duration = format_duration(elapsed)
99
+ except (ValueError, TypeError):
100
+ run_duration = "?"
101
+ content.append(f"🤖 RUNNING {run_duration}", style="bold yellow")
102
+ # Show supervision stats if available from monitor state
103
+ elif monitor_running and self.monitor_state.total_supervisions > 0:
104
+ content.append(f"sup:{self.monitor_state.total_supervisions}", style="magenta")
105
+ if self.monitor_state.supervisor_tokens > 0:
106
+ content.append(f" {format_tokens(self.monitor_state.supervisor_tokens)}", style="blue")
107
+ # Show cumulative daemon Claude run time
108
+ if self.monitor_state.supervisor_claude_total_run_seconds > 0:
109
+ total_run = format_duration(self.monitor_state.supervisor_claude_total_run_seconds)
110
+ content.append(f" ⏱{total_run}", style="dim")
111
+ else:
112
+ content.append("ready", style="green")
113
+ else:
114
+ content.append("○ ", style="red")
115
+ content.append("stopped", style="red")
116
+
117
+ # AI Summarizer status (from TUI's local summarizer, not daemon)
118
+ content.append(" │ ", style="dim")
119
+ content.append("AI: ", style="bold")
120
+ # Get summarizer state from parent app
121
+ summarizer_available = SummarizerClient.is_available()
122
+ summarizer_enabled = False
123
+ summarizer_calls = 0
124
+ if hasattr(self.app, '_summarizer'):
125
+ summarizer_enabled = self.app._summarizer.enabled
126
+ summarizer_calls = self.app._summarizer.total_calls
127
+ if summarizer_available:
128
+ if summarizer_enabled:
129
+ content.append("● ", style="green")
130
+ if summarizer_calls > 0:
131
+ content.append(f"{summarizer_calls}", style="cyan")
132
+ else:
133
+ content.append("on", style="green")
134
+ else:
135
+ content.append("○ ", style="dim")
136
+ content.append("off", style="dim")
137
+ else:
138
+ content.append("○ ", style="red")
139
+ content.append("n/a", style="red dim")
140
+
141
+ # Spin rate stats (only when monitor running with sessions)
142
+ if monitor_running and self.monitor_state.sessions:
143
+ content.append(" │ ", style="dim")
144
+ # Filter out sleeping agents from stats
145
+ all_sessions = self.monitor_state.sessions
146
+ active_sessions = [s for s in all_sessions if s.session_id not in self._asleep_session_ids]
147
+ sleeping_count = len(all_sessions) - len(active_sessions)
148
+
149
+ total_agents = len(active_sessions)
150
+ # Recalculate green_now excluding sleeping agents
151
+ green_now = sum(1 for s in active_sessions if s.current_status == "running")
152
+
153
+ content.append("Spin: ", style="bold")
154
+ content.append(f"{green_now}", style="bold green" if green_now > 0 else "dim")
155
+ content.append(f"/{total_agents}", style="dim")
156
+ if sleeping_count > 0:
157
+ content.append(f" 💤{sleeping_count}", style="dim") # Show sleeping count
158
+
159
+ # Mean spin rate - use history-based calculation if baseline > 0
160
+ baseline_minutes = getattr(self.app, 'baseline_minutes', 0)
161
+ if baseline_minutes > 0:
162
+ # History-based calculation for time window
163
+ history = read_agent_status_history(
164
+ hours=baseline_minutes / 60.0 + 0.1, # slight buffer
165
+ history_file=get_agent_history_path(self.tmux_session)
166
+ )
167
+ agent_names = [s.name for s in active_sessions]
168
+ mean_spin, sample_count = calculate_mean_spin_from_history(
169
+ history, agent_names, baseline_minutes
170
+ )
171
+
172
+ if sample_count > 0:
173
+ # Format window label: "15m", "1h", "1h30m"
174
+ if baseline_minutes < 60:
175
+ window_label = f"{baseline_minutes}m"
176
+ else:
177
+ hours = baseline_minutes // 60
178
+ mins = baseline_minutes % 60
179
+ window_label = f"{hours}h" if mins == 0 else f"{hours}h{mins}m"
180
+ content.append(f" μ{mean_spin:.1f} ({window_label})", style="cyan")
181
+ else:
182
+ content.append(" μ-- (no data)", style="dim")
183
+ else:
184
+ # Instantaneous: show current running count as the mean
185
+ content.append(f" μ{green_now}", style="cyan")
186
+
187
+ # Total tokens/cost across all sessions (include sleeping agents - they used tokens too)
188
+ if self.show_cost:
189
+ total_cost = sum(s.estimated_cost_usd for s in all_sessions)
190
+ if total_cost > 0:
191
+ content.append(f" {format_cost(total_cost)}", style="orange1")
192
+ else:
193
+ total_tokens = sum(s.input_tokens + s.output_tokens for s in all_sessions)
194
+ if total_tokens > 0:
195
+ content.append(f" Σ{format_tokens(total_tokens)}", style="orange1")
196
+
197
+ # Safe break duration (time until 50%+ agents need attention) - exclude sleeping
198
+ safe_break = calculate_safe_break_duration(active_sessions)
199
+ if safe_break is not None:
200
+ content.append(" │ ", style="dim")
201
+ content.append("☕", style="bold")
202
+ if safe_break < 60:
203
+ content.append(f" <1m", style="bold red")
204
+ elif safe_break < 300: # < 5 min
205
+ content.append(f" {format_duration(safe_break)}", style="bold yellow")
206
+ else:
207
+ content.append(f" {format_duration(safe_break)}", style="bold green")
208
+
209
+ # Presence status (only show if available via monitor daemon on macOS)
210
+ if monitor_running and self.monitor_state.presence_available:
211
+ content.append(" │ ", style="dim")
212
+ state = self.monitor_state.presence_state
213
+ idle = self.monitor_state.presence_idle_seconds or 0
214
+
215
+ state_names = {1: "🔒", 2: "💤", 3: "👤"}
216
+ state_colors = {1: "red", 2: "yellow", 3: "green"}
217
+
218
+ icon = state_names.get(state, "?")
219
+ color = state_colors.get(state, "dim")
220
+ content.append(f"{icon}", style=color)
221
+ content.append(f" {int(idle)}s", style="dim")
222
+
223
+ # Relay status (small indicator)
224
+ if monitor_running and self.monitor_state.relay_enabled:
225
+ content.append(" │ ", style="dim")
226
+ relay_status = self.monitor_state.relay_last_status
227
+ if relay_status == "ok":
228
+ content.append("📡", style="green")
229
+ elif relay_status == "error":
230
+ content.append("📡", style="red")
231
+ else:
232
+ content.append("📡", style="dim")
233
+
234
+ # Web server status
235
+ web_running = is_web_server_running(self.tmux_session)
236
+ if web_running:
237
+ content.append(" │ ", style="dim")
238
+ url = get_web_server_url(self.tmux_session)
239
+ content.append("🌐", style="green")
240
+ if url:
241
+ # Just show port
242
+ port = url.split(":")[-1] if url else ""
243
+ content.append(f":{port}", style="cyan")
244
+
245
+ return content
@@ -0,0 +1,71 @@
1
+ """
2
+ Help overlay widget for TUI.
3
+
4
+ Displays keyboard shortcuts and status color explanations.
5
+ """
6
+
7
+ from textual.widgets import Static
8
+ from rich.text import Text
9
+
10
+
11
+ class HelpOverlay(Static):
12
+ """Help overlay explaining all TUI metrics and controls"""
13
+
14
+ HELP_TEXT = """
15
+ ╔══════════════════════════════════════════════════════════════════════════════╗
16
+ ║ OVERCODE MONITOR HELP ║
17
+ ╠══════════════════════════════════════════════════════════════════════════════╣
18
+ ║ STATUS COLORS ║
19
+ ║ ────────────────────────────────────────────────────────────────────────── ║
20
+ ║ 🟢 Running 🟡 No orders 🟠 Wait supervisor 🔴 Wait user ║
21
+ ║ 💤 Asleep ⚫ Terminated ║
22
+ ║ ║
23
+ ║ NAVIGATION & VIEW ║
24
+ ║ ────────────────────────────────────────────────────────────────────────── ║
25
+ ║ j/↓ Next agent k/↑ Previous agent ║
26
+ ║ space Toggle expand m Toggle tree/list mode ║
27
+ ║ e Expand/Collapse all c Sync to main + clear ║
28
+ ║ h/? Toggle help r Refresh ║
29
+ ║ q Quit ║
30
+ ║ ║
31
+ ║ DISPLAY MODES ║
32
+ ║ ────────────────────────────────────────────────────────────────────────── ║
33
+ ║ s Cycle summary detail (low → med → full) ║
34
+ ║ l Cycle summary content (💬 short → 📖 context → 🎯 orders → ✏️ note)║
35
+ ║ v Cycle detail lines (5 → 10 → 20 → 50) ║
36
+ ║ S Cycle sort mode (alpha → status → value) ║
37
+ ║ t Toggle timeline d Toggle daemon panel ║
38
+ ║ g Show killed agents Z Hide sleeping agents ║
39
+ ║ ,/. Baseline time -/+15m 0 Reset baseline to now ║
40
+ ║ ║
41
+ ║ AGENT CONTROL ║
42
+ ║ ────────────────────────────────────────────────────────────────────────── ║
43
+ ║ i/: Send instruction o Set standing orders ║
44
+ ║ I Edit annotation Enter Approve (send Enter) ║
45
+ ║ 1-5 Send number n New agent ║
46
+ ║ x Kill agent R Restart agent ║
47
+ ║ z Toggle sleep V Edit agent value ║
48
+ ║ b Jump to red/attention H Handover all (2x) → draft PR ║
49
+ ║ ║
50
+ ║ DAEMON CONTROL ║
51
+ ║ ────────────────────────────────────────────────────────────────────────── ║
52
+ ║ [ Start supervisor ] Stop supervisor ║
53
+ ║ \\ Restart monitor w Toggle web dashboard ║
54
+ ║ ║
55
+ ║ OTHER ║
56
+ ║ ────────────────────────────────────────────────────────────────────────── ║
57
+ ║ y Copy mode (mouse sel) p Sync to tmux pane ║
58
+ ║ M Monochrome mode (for terminals with ANSI issues) ║
59
+ ║ ║
60
+ ║ COMMAND BAR (i or :) ║
61
+ ║ ────────────────────────────────────────────────────────────────────────── ║
62
+ ║ Enter Send instruction Esc Clear & unfocus ║
63
+ ║ Ctrl+E Multi-line mode Ctrl+O Set as standing order ║
64
+ ║ Ctrl+Enter Send (multi-line) ║
65
+ ║ ║
66
+ ║ Press h or ? to close ║
67
+ ╚══════════════════════════════════════════════════════════════════════════════╝
68
+ """
69
+
70
+ def render(self) -> Text:
71
+ return Text(self.HELP_TEXT.strip())
@@ -0,0 +1,69 @@
1
+ """
2
+ Preview pane widget for TUI.
3
+
4
+ Shows focused agent's terminal output in list+preview mode.
5
+ """
6
+
7
+ from typing import List, TYPE_CHECKING
8
+
9
+ from textual.widgets import Static
10
+ from textual.reactive import reactive
11
+ from rich.text import Text
12
+
13
+ if TYPE_CHECKING:
14
+ from .session_summary import SessionSummary
15
+
16
+
17
+ class PreviewPane(Static):
18
+ """Preview pane showing focused agent's terminal output in list+preview mode."""
19
+
20
+ content_lines: reactive[List[str]] = reactive(list, init=False)
21
+ monochrome: reactive[bool] = reactive(False)
22
+ session_name: str = ""
23
+
24
+ def __init__(self, **kwargs):
25
+ super().__init__(**kwargs)
26
+ self.content_lines = []
27
+
28
+ def render(self) -> Text:
29
+ content = Text()
30
+ # Use widget width for layout, with sensible fallback
31
+ pane_width = self.size.width if self.size.width > 0 else 80
32
+
33
+ # Header with session name - pad to full pane width
34
+ header = f"─── {self.session_name} " if self.session_name else "─── Preview "
35
+ header_style = "bold" if self.monochrome else "bold cyan"
36
+ border_style = "dim" if self.monochrome else "dim"
37
+ content.append(header, style=header_style)
38
+ content.append("─" * max(0, pane_width - len(header)), style=border_style)
39
+ content.append("\n")
40
+
41
+ if not self.content_lines:
42
+ content.append("(no output)", style="dim italic")
43
+ else:
44
+ # Calculate available lines based on widget height
45
+ # Reserve 2 lines for header and some padding
46
+ available_lines = max(10, self.size.height - 2) if self.size.height > 0 else 30
47
+ # Show last N lines of output with ANSI color support
48
+ # Truncate lines to pane width to match tmux display
49
+ max_line_len = max(pane_width - 1, 40) # Leave room for newline, minimum 40
50
+ for line in self.content_lines[-available_lines:]:
51
+ # Truncate long lines to pane width
52
+ display_line = line[:max_line_len] if len(line) > max_line_len else line
53
+ if self.monochrome:
54
+ # Strip ANSI colors - use plain text only
55
+ parsed = Text.from_ansi(display_line)
56
+ content.append(parsed.plain)
57
+ else:
58
+ # Parse ANSI escape sequences to preserve colors from tmux
59
+ # Note: Text.from_ansi() strips trailing newlines, so add newline separately
60
+ content.append(Text.from_ansi(display_line))
61
+ content.append("\n")
62
+
63
+ return content
64
+
65
+ def update_from_widget(self, widget: "SessionSummary") -> None:
66
+ """Update preview content from a SessionSummary widget."""
67
+ self.session_name = widget.session.name
68
+ self.content_lines = list(widget.pane_content) if widget.pane_content else []
69
+ self.refresh()