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
overcode/tui_render.py ADDED
@@ -0,0 +1,414 @@
1
+ """
2
+ Pure render functions for TUI components.
3
+
4
+ These functions are extracted from TUI widgets to enable unit testing
5
+ without requiring the full Textual framework.
6
+
7
+ All functions are pure - they take data as input and return Rich Text objects.
8
+ No side effects, no external dependencies.
9
+ """
10
+
11
+ from datetime import datetime
12
+ from typing import Optional, List, Dict, Tuple
13
+ from rich.text import Text
14
+
15
+ from .tui_helpers import (
16
+ format_interval,
17
+ format_duration,
18
+ format_tokens,
19
+ format_cost,
20
+ format_line_count,
21
+ get_status_symbol,
22
+ get_daemon_status_style,
23
+ calculate_uptime,
24
+ get_current_state_times,
25
+ )
26
+ from .settings import DAEMON_VERSION
27
+
28
+
29
+ def render_daemon_monitor_section(
30
+ monitor_state, # MonitorDaemonState or None
31
+ is_stale: bool,
32
+ ) -> Text:
33
+ """Render the Monitor Daemon section of the status bar.
34
+
35
+ Args:
36
+ monitor_state: Monitor daemon state object or None
37
+ is_stale: Whether the state is considered stale
38
+
39
+ Returns:
40
+ Rich Text for the monitor section
41
+ """
42
+ content = Text()
43
+ content.append("Monitor: ", style="bold")
44
+
45
+ monitor_running = monitor_state is not None and not is_stale
46
+
47
+ if monitor_running:
48
+ symbol, style = get_daemon_status_style(monitor_state.status)
49
+ content.append(f"{symbol} ", style=style)
50
+ content.append(f"#{monitor_state.loop_count}", style="cyan")
51
+ content.append(f" @{format_interval(monitor_state.current_interval)}", style="dim")
52
+
53
+ # Version mismatch warning
54
+ if monitor_state.daemon_version != DAEMON_VERSION:
55
+ content.append(
56
+ f" ⚠v{monitor_state.daemon_version}→{DAEMON_VERSION}",
57
+ style="bold yellow"
58
+ )
59
+ else:
60
+ content.append("○ ", style="red")
61
+ content.append("stopped", style="red")
62
+
63
+ return content
64
+
65
+
66
+ def render_supervisor_section(
67
+ supervisor_running: bool,
68
+ monitor_state, # MonitorDaemonState or None
69
+ is_monitor_running: bool,
70
+ ) -> Text:
71
+ """Render the Supervisor Daemon section of the status bar.
72
+
73
+ Args:
74
+ supervisor_running: Whether supervisor daemon is running
75
+ monitor_state: Monitor daemon state (for supervisor claude info)
76
+ is_monitor_running: Whether monitor daemon is running
77
+
78
+ Returns:
79
+ Rich Text for the supervisor section
80
+ """
81
+ content = Text()
82
+ content.append("Supervisor: ", style="bold")
83
+
84
+ if supervisor_running:
85
+ content.append("● ", style="green")
86
+
87
+ if is_monitor_running and monitor_state and monitor_state.supervisor_claude_running:
88
+ # Calculate current run duration
89
+ run_duration = ""
90
+ if monitor_state.supervisor_claude_started_at:
91
+ try:
92
+ started = datetime.fromisoformat(monitor_state.supervisor_claude_started_at)
93
+ elapsed = (datetime.now() - started).total_seconds()
94
+ run_duration = format_duration(elapsed)
95
+ except (ValueError, TypeError):
96
+ run_duration = "?"
97
+ content.append(f"🤖 RUNNING {run_duration}", style="bold yellow")
98
+
99
+ elif is_monitor_running and monitor_state and monitor_state.total_supervisions > 0:
100
+ content.append(f"sup:{monitor_state.total_supervisions}", style="magenta")
101
+ if monitor_state.supervisor_tokens > 0:
102
+ content.append(f" {format_tokens(monitor_state.supervisor_tokens)}", style="blue")
103
+ if monitor_state.supervisor_claude_total_run_seconds > 0:
104
+ total_run = format_duration(monitor_state.supervisor_claude_total_run_seconds)
105
+ content.append(f" ⏱{total_run}", style="dim")
106
+ else:
107
+ content.append("ready", style="green")
108
+ else:
109
+ content.append("○ ", style="red")
110
+ content.append("stopped", style="red")
111
+
112
+ return content
113
+
114
+
115
+ def render_ai_summarizer_section(
116
+ summarizer_available: bool,
117
+ summarizer_enabled: bool,
118
+ summarizer_calls: int,
119
+ ) -> Text:
120
+ """Render the AI Summarizer section of the status bar.
121
+
122
+ Args:
123
+ summarizer_available: Whether API key is available
124
+ summarizer_enabled: Whether summarizer is enabled
125
+ summarizer_calls: Number of API calls made
126
+
127
+ Returns:
128
+ Rich Text for the AI section
129
+ """
130
+ content = Text()
131
+ content.append("AI: ", style="bold")
132
+
133
+ if summarizer_available:
134
+ if summarizer_enabled:
135
+ content.append("● ", style="green")
136
+ if summarizer_calls > 0:
137
+ content.append(f"{summarizer_calls}", style="cyan")
138
+ else:
139
+ content.append("on", style="green")
140
+ else:
141
+ content.append("○ ", style="dim")
142
+ content.append("off", style="dim")
143
+ else:
144
+ content.append("○ ", style="red")
145
+ content.append("n/a", style="red dim")
146
+
147
+ return content
148
+
149
+
150
+ def render_spin_stats(
151
+ sessions: List, # List of SessionDaemonState
152
+ asleep_session_ids: set,
153
+ show_cost: bool = False,
154
+ ) -> Text:
155
+ """Render spin rate statistics.
156
+
157
+ Args:
158
+ sessions: List of session daemon states
159
+ asleep_session_ids: Set of session IDs that are asleep
160
+ show_cost: Show $ cost instead of token counts
161
+
162
+ Returns:
163
+ Rich Text for spin stats section
164
+ """
165
+ content = Text()
166
+
167
+ # Filter out sleeping agents
168
+ active_sessions = [s for s in sessions if s.session_id not in asleep_session_ids]
169
+ sleeping_count = len(sessions) - len(active_sessions)
170
+
171
+ total_agents = len(active_sessions)
172
+ green_now = sum(1 for s in active_sessions if s.current_status == "running")
173
+
174
+ # Calculate mean spin rate
175
+ mean_spin = 0.0
176
+ for s in active_sessions:
177
+ total_time = s.green_time_seconds + s.non_green_time_seconds
178
+ if total_time > 0:
179
+ mean_spin += s.green_time_seconds / total_time
180
+
181
+ content.append("Spin: ", style="bold")
182
+ content.append(f"{green_now}", style="bold green" if green_now > 0 else "dim")
183
+ content.append(f"/{total_agents}", style="dim")
184
+
185
+ if sleeping_count > 0:
186
+ content.append(f" 💤{sleeping_count}", style="dim")
187
+
188
+ if mean_spin > 0:
189
+ content.append(f" μ{mean_spin:.1f}x", style="cyan")
190
+
191
+ # Total tokens/cost (include sleeping agents)
192
+ if show_cost:
193
+ total_cost = sum(s.estimated_cost_usd for s in sessions)
194
+ if total_cost > 0:
195
+ content.append(f" {format_cost(total_cost)}", style="orange1")
196
+ else:
197
+ total_tokens = sum(s.input_tokens + s.output_tokens for s in sessions)
198
+ if total_tokens > 0:
199
+ content.append(f" Σ{format_tokens(total_tokens)}", style="orange1")
200
+
201
+ return content
202
+
203
+
204
+ def render_presence_indicator(
205
+ presence_state: int,
206
+ idle_seconds: float,
207
+ ) -> Text:
208
+ """Render presence status indicator.
209
+
210
+ Args:
211
+ presence_state: 1=locked, 2=inactive, 3=active
212
+ idle_seconds: Seconds since last activity
213
+
214
+ Returns:
215
+ Rich Text for presence indicator
216
+ """
217
+ content = Text()
218
+
219
+ state_icons = {1: "🔒", 2: "💤", 3: "👤"}
220
+ state_colors = {1: "red", 2: "yellow", 3: "green"}
221
+
222
+ icon = state_icons.get(presence_state, "?")
223
+ color = state_colors.get(presence_state, "dim")
224
+
225
+ content.append(f"{icon}", style=color)
226
+ content.append(f" {int(idle_seconds)}s", style="dim")
227
+
228
+ return content
229
+
230
+
231
+ def render_session_summary_line(
232
+ name: str,
233
+ detected_status: str,
234
+ expanded: bool,
235
+ summary_detail: str, # "low", "med", "full"
236
+ start_time: str,
237
+ repo_name: Optional[str],
238
+ branch: Optional[str],
239
+ green_time: float,
240
+ non_green_time: float,
241
+ permissiveness_mode: str,
242
+ state_since: Optional[str],
243
+ local_status_changed_at: Optional[datetime],
244
+ steers_count: int,
245
+ total_tokens: Optional[int],
246
+ current_context_tokens: Optional[int],
247
+ interaction_count: Optional[int],
248
+ median_work_time: float,
249
+ git_diff_stats: Optional[Tuple[int, int, int]],
250
+ is_unvisited_stalled: bool,
251
+ has_focus: bool,
252
+ is_list_mode: bool,
253
+ max_repo_info_width: int = 18,
254
+ show_cost: bool = False,
255
+ estimated_cost_usd: float = 0.0,
256
+ ) -> Text:
257
+ """Render a single session summary line.
258
+
259
+ This is a pure function that builds the Rich Text display for a session.
260
+ All data is passed as parameters, no external dependencies.
261
+
262
+ Args:
263
+ name: Session name
264
+ detected_status: Current detected status string
265
+ expanded: Whether session is expanded
266
+ summary_detail: Detail level
267
+ start_time: ISO timestamp of session start
268
+ repo_name: Repository name
269
+ branch: Git branch
270
+ green_time: Total green (running) time in seconds
271
+ non_green_time: Total non-green time in seconds
272
+ permissiveness_mode: "normal", "permissive", or "bypass"
273
+ state_since: ISO timestamp of current state start
274
+ local_status_changed_at: Local datetime when status changed
275
+ steers_count: Number of robot supervisions
276
+ total_tokens: Total tokens used (or None)
277
+ current_context_tokens: Current context window usage (or None)
278
+ interaction_count: Total interactions (or None)
279
+ median_work_time: Median autonomous work time
280
+ git_diff_stats: Tuple of (files, insertions, deletions) or None
281
+ is_unvisited_stalled: Whether this is an unvisited stalled agent
282
+ has_focus: Whether this widget has focus
283
+ is_list_mode: Whether in list view mode
284
+ max_repo_info_width: Width for repo info column
285
+ show_cost: Show $ cost instead of token counts
286
+ estimated_cost_usd: Estimated cost in USD
287
+
288
+ Returns:
289
+ Rich Text object for the summary line
290
+ """
291
+ bg = " on #0d2137"
292
+
293
+ # Expansion indicator
294
+ expand_icon = "▼" if expanded else "▶"
295
+
296
+ # Calculate values
297
+ uptime = calculate_uptime(start_time)
298
+ repo_info = f"{repo_name or 'n/a'}:{branch or 'n/a'}"
299
+
300
+ # Status indicator
301
+ status_symbol, base_color = get_status_symbol(detected_status)
302
+ status_color = f"bold {base_color}{bg}"
303
+
304
+ # Permissiveness emoji
305
+ perm_emojis = {"bypass": "🔥", "permissive": "🏃"}
306
+ perm_emoji = perm_emojis.get(permissiveness_mode, "👮")
307
+
308
+ content = Text()
309
+
310
+ # Name width based on detail level
311
+ name_widths = {"low": 24, "med": 20, "full": 16}
312
+ name_width = name_widths.get(summary_detail, 20)
313
+ display_name = name[:name_width].ljust(name_width)
314
+
315
+ # Status symbol
316
+ content.append(f"{status_symbol} ", style=status_color)
317
+
318
+ # Stalled indicator
319
+ if is_unvisited_stalled:
320
+ content.append("🔔", style=f"bold blink red{bg}")
321
+ else:
322
+ content.append(" ", style=f"dim{bg}")
323
+
324
+ # Time in current state
325
+ state_start = local_status_changed_at
326
+ if state_since:
327
+ try:
328
+ daemon_state_start = datetime.fromisoformat(state_since)
329
+ if state_start is None or daemon_state_start > state_start:
330
+ state_start = daemon_state_start
331
+ except (ValueError, TypeError):
332
+ pass
333
+
334
+ if state_start:
335
+ elapsed = (datetime.now() - state_start).total_seconds()
336
+ content.append(f"{format_duration(elapsed):>5} ", style=status_color)
337
+ else:
338
+ content.append(" - ", style=f"dim{bg}")
339
+
340
+ # Focus/expand indicator
341
+ if is_list_mode:
342
+ if has_focus:
343
+ content.append("→ ", style=status_color)
344
+ else:
345
+ content.append(" ", style=status_color)
346
+ else:
347
+ content.append(f"{expand_icon} ", style=status_color)
348
+
349
+ content.append(f"{display_name}", style=f"bold cyan{bg}")
350
+
351
+ # Full detail: repo:branch
352
+ if summary_detail == "full":
353
+ content.append(f" {repo_info:<{max_repo_info_width}} ", style=f"bold dim{bg}")
354
+
355
+ # Med/Full: uptime, times
356
+ if summary_detail in ("med", "full"):
357
+ content.append(f" ↑{uptime:>5}", style=f"bold white{bg}")
358
+ content.append(f" ▶{format_duration(green_time):>5}", style=f"bold green{bg}")
359
+ content.append(f" ⏸{format_duration(non_green_time):>5}", style=f"bold red{bg}")
360
+
361
+ if summary_detail == "full":
362
+ total_time = green_time + non_green_time
363
+ pct = (green_time / total_time * 100) if total_time > 0 else 0
364
+ pct_style = f"bold green{bg}" if pct >= 50 else f"bold red{bg}"
365
+ content.append(f" {pct:>3.0f}%", style=pct_style)
366
+
367
+ # Token usage or cost
368
+ if total_tokens is not None:
369
+ if show_cost:
370
+ content.append(f" {format_cost(estimated_cost_usd):>7}", style=f"bold orange1{bg}")
371
+ else:
372
+ content.append(f" Σ{format_tokens(total_tokens):>6}", style=f"bold orange1{bg}")
373
+ if current_context_tokens and current_context_tokens > 0:
374
+ max_context = 200_000
375
+ ctx_pct = min(100, current_context_tokens / max_context * 100)
376
+ content.append(f" c@{ctx_pct:>3.0f}%", style=f"bold orange1{bg}")
377
+ else:
378
+ content.append(" c@ -%", style=f"dim orange1{bg}")
379
+ else:
380
+ content.append(" - c@ -%", style=f"dim orange1{bg}")
381
+
382
+ # Git diff stats
383
+ if git_diff_stats:
384
+ files, ins, dels = git_diff_stats
385
+ if summary_detail == "full":
386
+ content.append(f" Δ{files:>2}", style=f"bold magenta{bg}")
387
+ content.append(f" +{format_line_count(ins):>4}", style=f"bold green{bg}")
388
+ content.append(f" -{format_line_count(dels):>4}", style=f"bold red{bg}")
389
+ else:
390
+ style = f"bold magenta{bg}" if files > 0 else f"dim{bg}"
391
+ content.append(f" Δ{files:>2}", style=style)
392
+ else:
393
+ if summary_detail == "full":
394
+ content.append(" Δ- + - - ", style=f"dim{bg}")
395
+ else:
396
+ content.append(" Δ-", style=f"dim{bg}")
397
+
398
+ # Med/Full: median work time
399
+ if summary_detail in ("med", "full"):
400
+ work_str = format_duration(median_work_time) if median_work_time > 0 else "0s"
401
+ content.append(f" ⏱{work_str:>5}", style=f"bold blue{bg}")
402
+
403
+ # Permission mode, human/robot counts
404
+ content.append(f" {perm_emoji}", style=f"bold white{bg}")
405
+
406
+ if interaction_count is not None:
407
+ human_count = max(0, interaction_count - steers_count)
408
+ content.append(f" 👤{human_count:>3}", style=f"bold yellow{bg}")
409
+ else:
410
+ content.append(" 👤 -", style=f"dim yellow{bg}")
411
+
412
+ content.append(f" 🤖{steers_count:>3}", style=f"bold cyan{bg}")
413
+
414
+ return content
@@ -0,0 +1,24 @@
1
+ """
2
+ TUI Widget components for Overcode.
3
+
4
+ This package contains the individual widget classes extracted from tui.py
5
+ for better maintainability and testability.
6
+ """
7
+
8
+ from .help_overlay import HelpOverlay
9
+ from .preview_pane import PreviewPane
10
+ from .daemon_panel import DaemonPanel
11
+ from .daemon_status_bar import DaemonStatusBar
12
+ from .status_timeline import StatusTimeline
13
+ from .session_summary import SessionSummary
14
+ from .command_bar import CommandBar
15
+
16
+ __all__ = [
17
+ "HelpOverlay",
18
+ "PreviewPane",
19
+ "DaemonPanel",
20
+ "DaemonStatusBar",
21
+ "StatusTimeline",
22
+ "SessionSummary",
23
+ "CommandBar",
24
+ ]