overcode 0.1.0__tar.gz → 0.1.2__tar.gz

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 (57) hide show
  1. {overcode-0.1.0/src/overcode.egg-info → overcode-0.1.2}/PKG-INFO +13 -1
  2. {overcode-0.1.0 → overcode-0.1.2}/README.md +12 -0
  3. {overcode-0.1.0 → overcode-0.1.2}/pyproject.toml +1 -1
  4. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/__init__.py +1 -1
  5. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/cli.py +42 -3
  6. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/config.py +49 -0
  7. overcode-0.1.2/src/overcode/daemon_logging.py +144 -0
  8. overcode-0.1.2/src/overcode/daemon_utils.py +84 -0
  9. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/history_reader.py +17 -5
  10. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/implementations.py +11 -0
  11. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/launcher.py +3 -0
  12. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/mocks.py +4 -0
  13. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/monitor_daemon.py +25 -126
  14. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/pid_utils.py +10 -3
  15. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/protocols.py +12 -0
  16. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/session_manager.py +3 -0
  17. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/settings.py +20 -1
  18. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/standing_instructions.py +15 -6
  19. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_constants.py +11 -0
  20. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_detector.py +38 -0
  21. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_patterns.py +12 -0
  22. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/supervisor_daemon.py +40 -171
  23. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/tui.py +326 -39
  24. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/tui_helpers.py +18 -0
  25. overcode-0.1.2/src/overcode/web_api.py +763 -0
  26. overcode-0.1.2/src/overcode/web_chartjs.py +32 -0
  27. overcode-0.1.2/src/overcode/web_server.py +490 -0
  28. overcode-0.1.2/src/overcode/web_server_runner.py +104 -0
  29. overcode-0.1.2/src/overcode/web_templates.py +1656 -0
  30. {overcode-0.1.0 → overcode-0.1.2/src/overcode.egg-info}/PKG-INFO +13 -1
  31. {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/SOURCES.txt +4 -2
  32. overcode-0.1.0/src/overcode/daemon.py +0 -1184
  33. overcode-0.1.0/src/overcode/daemon_state.py +0 -113
  34. overcode-0.1.0/src/overcode/web_api.py +0 -279
  35. overcode-0.1.0/src/overcode/web_server.py +0 -138
  36. overcode-0.1.0/src/overcode/web_templates.py +0 -563
  37. {overcode-0.1.0 → overcode-0.1.2}/LICENSE +0 -0
  38. {overcode-0.1.0 → overcode-0.1.2}/MANIFEST.in +0 -0
  39. {overcode-0.1.0 → overcode-0.1.2}/setup.cfg +0 -0
  40. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/daemon_claude_skill.md +0 -0
  41. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/data_export.py +0 -0
  42. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/dependency_check.py +0 -0
  43. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/exceptions.py +0 -0
  44. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/interfaces.py +0 -0
  45. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/logging_config.py +0 -0
  46. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/monitor_daemon_state.py +0 -0
  47. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/presence_logger.py +0 -0
  48. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/status_history.py +0 -0
  49. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/summarizer_client.py +0 -0
  50. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/summarizer_component.py +0 -0
  51. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/supervisor_layout.sh +0 -0
  52. {overcode-0.1.0 → overcode-0.1.2}/src/overcode/tmux_manager.py +0 -0
  53. {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/dependency_links.txt +0 -0
  54. {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/entry_points.txt +0 -0
  55. {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/requires.txt +0 -0
  56. {overcode-0.1.0 → overcode-0.1.2}/src/overcode.egg-info/top_level.txt +0 -0
  57. {overcode-0.1.0 → overcode-0.1.2}/tests/test_e2e_multi_agent_jokes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overcode
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A supervisor for managing multiple Claude Code instances in tmux
5
5
  Author: Mike Bond
6
6
  Project-URL: Homepage, https://github.com/mkb23/overcode
@@ -43,6 +43,18 @@ A TUI supervisor for managing multiple Claude Code agents in tmux.
43
43
 
44
44
  Monitor status, costs, and activity across all your agents from a single dashboard.
45
45
 
46
+ ## Screenshots
47
+
48
+ **Split-screen with tmux sync** - Monitor agents in the top pane while viewing live agent output below. Press `p` to enable pane sync, then navigate with `j/k` to switch the bottom pane to the selected agent's window.
49
+
50
+ ![Overcode split-screen with tmux sync](docs/images/overcode-split-screen.png)
51
+
52
+ > **iTerm2 setup**: Use `Cmd+Shift+D` to split horizontally. Run `overcode monitor` in the top pane and `tmux attach -t agents` in the bottom pane.
53
+
54
+ **Preview mode** - Press `m` to toggle List+Preview mode. Shows collapsed agent list with detailed terminal output preview for the selected agent.
55
+
56
+ ![Overcode preview mode](docs/images/overcode-preview-mode.png)
57
+
46
58
  ## Installation
47
59
 
48
60
  ```bash
@@ -4,6 +4,18 @@ A TUI supervisor for managing multiple Claude Code agents in tmux.
4
4
 
5
5
  Monitor status, costs, and activity across all your agents from a single dashboard.
6
6
 
7
+ ## Screenshots
8
+
9
+ **Split-screen with tmux sync** - Monitor agents in the top pane while viewing live agent output below. Press `p` to enable pane sync, then navigate with `j/k` to switch the bottom pane to the selected agent's window.
10
+
11
+ ![Overcode split-screen with tmux sync](docs/images/overcode-split-screen.png)
12
+
13
+ > **iTerm2 setup**: Use `Cmd+Shift+D` to split horizontally. Run `overcode monitor` in the top pane and `tmux attach -t agents` in the bottom pane.
14
+
15
+ **Preview mode** - Press `m` to toggle List+Preview mode. Shows collapsed agent list with detailed terminal output preview for the selected agent.
16
+
17
+ ![Overcode preview mode](docs/images/overcode-preview-mode.png)
18
+
7
19
  ## Installation
8
20
 
9
21
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overcode"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -2,4 +2,4 @@
2
2
  Overcode - A supervisor for managing multiple Claude Code instances.
3
3
  """
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.1.2"
@@ -264,7 +264,7 @@ def instruct(
264
264
  ] = None,
265
265
  instructions: Annotated[
266
266
  Optional[List[str]],
267
- typer.Argument(help="Instructions or preset name (e.g., DEFAULT, CODING)"),
267
+ typer.Argument(help="Instructions or preset name (e.g., DO_NOTHING, STANDARD, CODING)"),
268
268
  ] = None,
269
269
  clear: Annotated[
270
270
  bool, typer.Option("--clear", "-c", help="Clear standing instructions")
@@ -276,7 +276,7 @@ def instruct(
276
276
  ):
277
277
  """Set standing instructions for an agent.
278
278
 
279
- Use a preset name (DEFAULT, CODING, TESTING, etc.) or provide custom instructions.
279
+ Use a preset name (DO_NOTHING, STANDARD, CODING, etc.) or provide custom instructions.
280
280
  Use --list to see all available presets.
281
281
  """
282
282
  from .session_manager import SessionManager
@@ -285,7 +285,7 @@ def instruct(
285
285
  if list_presets:
286
286
  presets_dict = load_presets()
287
287
  rprint("\n[bold]Standing Instruction Presets:[/bold]\n")
288
- for preset_name in sorted(presets_dict.keys(), key=lambda x: (x != "DEFAULT", x)):
288
+ for preset_name in sorted(presets_dict.keys(), key=lambda x: (x != "DO_NOTHING", x)):
289
289
  preset = presets_dict[preset_name]
290
290
  rprint(f" [cyan]{preset_name:12}[/cyan] {preset.description}")
291
291
  rprint("\n[dim]Usage: overcode instruct <agent> <PRESET>[/dim]")
@@ -405,6 +405,45 @@ def serve(
405
405
  run_server(host=host, port=port, tmux_session=session)
406
406
 
407
407
 
408
+ @app.command()
409
+ def web(
410
+ host: Annotated[
411
+ str, typer.Option("--host", "-h", help="Host to bind to")
412
+ ] = "127.0.0.1",
413
+ port: Annotated[
414
+ int, typer.Option("--port", "-p", help="Port to listen on")
415
+ ] = 8080,
416
+ ):
417
+ """Launch analytics web dashboard for browsing historical data.
418
+
419
+ A lightweight web app for exploring session history, timeline
420
+ visualization, and efficiency metrics. Uses Chart.js for
421
+ interactive charts with dark theme matching the TUI.
422
+
423
+ Features:
424
+ - Dashboard with summary stats and daily activity charts
425
+ - Session browser with sortable table
426
+ - Timeline view with agent status and user presence
427
+ - Efficiency metrics with cost analysis
428
+
429
+ Time range presets can be configured in ~/.overcode/config.yaml:
430
+
431
+ web:
432
+ time_presets:
433
+ - name: "Morning"
434
+ start: "09:00"
435
+ end: "12:00"
436
+
437
+ Examples:
438
+ overcode web # Start on localhost:8080
439
+ overcode web --port 3000 # Custom port
440
+ overcode web --host 0.0.0.0 # Listen on all interfaces
441
+ """
442
+ from .web_server import run_analytics_server
443
+
444
+ run_analytics_server(host=host, port=port)
445
+
446
+
408
447
 
409
448
 
410
449
  @app.command()
@@ -70,3 +70,52 @@ def get_relay_config() -> Optional[dict]:
70
70
  "api_key": api_key,
71
71
  "interval": relay.get("interval", 30),
72
72
  }
73
+
74
+
75
+ def get_web_time_presets() -> list:
76
+ """Get time presets for the web analytics dashboard.
77
+
78
+ Returns list of preset dictionaries with name, start, end times.
79
+ Falls back to defaults if not configured.
80
+
81
+ Config format in ~/.overcode/config.yaml:
82
+ web:
83
+ time_presets:
84
+ - name: "Morning"
85
+ start: "09:00"
86
+ end: "12:00"
87
+ - name: "Full Day"
88
+ start: "09:00"
89
+ end: "17:00"
90
+ - name: "Night Owl"
91
+ start: "22:00"
92
+ end: "02:00"
93
+ """
94
+ config = load_config()
95
+ web_config = config.get("web", {})
96
+ presets = web_config.get("time_presets", None)
97
+
98
+ if presets and isinstance(presets, list):
99
+ # Validate and normalize presets
100
+ valid_presets = []
101
+ for p in presets:
102
+ if isinstance(p, dict) and "name" in p:
103
+ valid_presets.append({
104
+ "name": p.get("name", ""),
105
+ "start": p.get("start"),
106
+ "end": p.get("end"),
107
+ })
108
+ if valid_presets:
109
+ # Always add "All Time" at the end
110
+ if not any(p["name"] == "All Time" for p in valid_presets):
111
+ valid_presets.append({"name": "All Time", "start": None, "end": None})
112
+ return valid_presets
113
+
114
+ # Default presets
115
+ return [
116
+ {"name": "Morning", "start": "09:00", "end": "12:00"},
117
+ {"name": "Afternoon", "start": "13:00", "end": "17:00"},
118
+ {"name": "Full Day", "start": "09:00", "end": "17:00"},
119
+ {"name": "Evening", "start": "18:00", "end": "22:00"},
120
+ {"name": "All Time", "start": None, "end": None},
121
+ ]
@@ -0,0 +1,144 @@
1
+ """
2
+ Shared logging utilities for Overcode daemons.
3
+
4
+ Provides base logger class with common functionality for both
5
+ monitor_daemon and supervisor_daemon.
6
+ """
7
+
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ from rich.console import Console
13
+ from rich.text import Text
14
+ from rich.theme import Theme
15
+
16
+
17
+ # Shared theme for daemon logs
18
+ DAEMON_THEME = Theme({
19
+ "info": "cyan",
20
+ "warn": "yellow",
21
+ "error": "bold red",
22
+ "success": "bold green",
23
+ "daemon_claude": "magenta",
24
+ "dim": "dim white",
25
+ "highlight": "bold white",
26
+ })
27
+
28
+
29
+ class BaseDaemonLogger:
30
+ """Base logger for daemons with common logging methods."""
31
+
32
+ def __init__(self, log_file: Path, theme: Theme = None):
33
+ """Initialize the logger.
34
+
35
+ Args:
36
+ log_file: Path to the log file
37
+ theme: Optional Rich theme (defaults to DAEMON_THEME)
38
+ """
39
+ self.log_file = log_file
40
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
41
+ self.console = Console(theme=theme or DAEMON_THEME, force_terminal=True)
42
+
43
+ def _write_to_file(self, message: str, level: str = "INFO"):
44
+ """Write plain text to log file."""
45
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
46
+ line = f"[{timestamp}] [{level}] {message}"
47
+ try:
48
+ with open(self.log_file, 'a') as f:
49
+ f.write(line + '\n')
50
+ except OSError:
51
+ pass
52
+
53
+ def _log(self, style: str, prefix: str, message: str, level: str = "INFO"):
54
+ """Log a message with style to both console and file."""
55
+ timestamp = datetime.now().strftime("%H:%M:%S")
56
+ text = Text()
57
+ text.append(f"[{timestamp}] ", style="dim")
58
+ text.append(f"{prefix} ", style=style)
59
+ text.append(message)
60
+ self.console.print(text)
61
+ self._write_to_file(message, level)
62
+
63
+ def info(self, message: str):
64
+ """Log info message."""
65
+ self._log("info", "●", message, "INFO")
66
+
67
+ def warn(self, message: str):
68
+ """Log warning message."""
69
+ self._log("warn", "⚠", message, "WARN")
70
+
71
+ def error(self, message: str):
72
+ """Log error message."""
73
+ self._log("error", "✗", message, "ERROR")
74
+
75
+ def success(self, message: str):
76
+ """Log success message."""
77
+ self._log("success", "✓", message, "INFO")
78
+
79
+ def debug(self, message: str):
80
+ """Log a debug message (only to file, not console)."""
81
+ timestamp = datetime.now().strftime("%H:%M:%S")
82
+ try:
83
+ with open(self.log_file, 'a') as f:
84
+ f.write(f"[{timestamp}] DEBUG {message}\n")
85
+ except OSError:
86
+ pass
87
+
88
+ def section(self, title: str):
89
+ """Print a section header."""
90
+ self._write_to_file(f"=== {title} ===", "INFO")
91
+ self.console.print()
92
+ self.console.rule(f"[bold cyan]{title}[/]")
93
+
94
+
95
+ class SupervisorDaemonLogger(BaseDaemonLogger):
96
+ """Logger for supervisor daemon with additional methods."""
97
+
98
+ def __init__(self, log_file: Path):
99
+ super().__init__(log_file)
100
+ self._seen_daemon_claude_lines: set = set()
101
+
102
+ def daemon_claude_output(self, lines: List[str]):
103
+ """Log daemon claude output, showing only new lines."""
104
+ new_lines = []
105
+
106
+ for line in lines:
107
+ stripped = line.strip()
108
+ if not stripped:
109
+ continue
110
+ if stripped not in self._seen_daemon_claude_lines:
111
+ new_lines.append(stripped)
112
+ self._seen_daemon_claude_lines.add(stripped)
113
+
114
+ # Limit set size
115
+ if len(self._seen_daemon_claude_lines) > 500:
116
+ current_lines = {line.strip() for line in lines if line.strip()}
117
+ self._seen_daemon_claude_lines = current_lines
118
+
119
+ if new_lines:
120
+ for line in new_lines:
121
+ self._write_to_file(f"[DAEMON_CLAUDE] {line}", "INFO")
122
+ if line.startswith('✓') or 'success' in line.lower():
123
+ self.console.print(f" [success]│[/success] {line}")
124
+ elif line.startswith('✗') or 'error' in line.lower() or 'fail' in line.lower():
125
+ self.console.print(f" [error]│[/error] {line}")
126
+ elif line.startswith('>') or line.startswith('$'):
127
+ self.console.print(f" [highlight]│[/highlight] {line}")
128
+ else:
129
+ self.console.print(f" [daemon_claude]│[/daemon_claude] {line}")
130
+
131
+ def status_summary(self, total: int, green: int, non_green: int, loop: int):
132
+ """Print a status summary line."""
133
+ status_text = Text()
134
+ status_text.append(f"Loop #{loop}: ", style="dim")
135
+ status_text.append(f"{total} agents ", style="highlight")
136
+ status_text.append("(", style="dim")
137
+ status_text.append(f"{green} green", style="success")
138
+ status_text.append(", ", style="dim")
139
+ status_text.append(f"{non_green} non-green", style="warn" if non_green else "dim")
140
+ status_text.append(")", style="dim")
141
+
142
+ self._write_to_file(f"Loop #{loop}: {total} agents ({green} green, {non_green} non-green)", "INFO")
143
+ self.console.print(f"[dim]{datetime.now().strftime('%H:%M:%S')}[/dim] ", end="")
144
+ self.console.print(status_text)
@@ -0,0 +1,84 @@
1
+ """
2
+ Shared utilities for Overcode daemons.
3
+
4
+ Provides factory functions for creating daemon PID management helpers,
5
+ avoiding code duplication between monitor_daemon and supervisor_daemon.
6
+ """
7
+
8
+ import os
9
+ import signal
10
+ from pathlib import Path
11
+ from typing import Callable, Optional, Tuple
12
+
13
+ from .pid_utils import (
14
+ get_process_pid,
15
+ is_process_running,
16
+ remove_pid_file,
17
+ )
18
+ from .settings import DAEMON
19
+
20
+
21
+ def create_daemon_helpers(
22
+ get_pid_path: Callable[[str], Path],
23
+ daemon_name: str,
24
+ ) -> Tuple[
25
+ Callable[[Optional[str]], bool],
26
+ Callable[[Optional[str]], Optional[int]],
27
+ Callable[[Optional[str]], bool],
28
+ ]:
29
+ """Factory to create is_*_running, get_*_pid, stop_* functions for a daemon.
30
+
31
+ Args:
32
+ get_pid_path: Function that takes session name and returns PID file path
33
+ daemon_name: Human-readable name for error messages
34
+
35
+ Returns:
36
+ Tuple of (is_running_fn, get_pid_fn, stop_fn)
37
+ """
38
+
39
+ def is_running(session: str = None) -> bool:
40
+ """Check if the daemon process is currently running for a session.
41
+
42
+ Args:
43
+ session: tmux session name (default: from config)
44
+ """
45
+ if session is None:
46
+ session = DAEMON.default_tmux_session
47
+ return is_process_running(get_pid_path(session))
48
+
49
+ def get_pid(session: str = None) -> Optional[int]:
50
+ """Get the daemon PID if running, None otherwise.
51
+
52
+ Args:
53
+ session: tmux session name (default: from config)
54
+ """
55
+ if session is None:
56
+ session = DAEMON.default_tmux_session
57
+ return get_process_pid(get_pid_path(session))
58
+
59
+ def stop(session: str = None) -> bool:
60
+ """Stop the daemon process if running.
61
+
62
+ Args:
63
+ session: tmux session name (default: from config)
64
+
65
+ Returns:
66
+ True if daemon was stopped, False if it wasn't running.
67
+ """
68
+ if session is None:
69
+ session = DAEMON.default_tmux_session
70
+ pid_path = get_pid_path(session)
71
+ pid = get_process_pid(pid_path)
72
+ if pid is None:
73
+ remove_pid_file(pid_path)
74
+ return False
75
+
76
+ try:
77
+ os.kill(pid, signal.SIGTERM)
78
+ remove_pid_file(pid_path)
79
+ return True
80
+ except (OSError, ProcessLookupError):
81
+ remove_pid_file(pid_path)
82
+ return False
83
+
84
+ return is_running, get_pid, stop
@@ -39,6 +39,7 @@ class ClaudeSessionStats:
39
39
  cache_creation_tokens: int
40
40
  cache_read_tokens: int
41
41
  work_times: List[float] # seconds per work cycle (prompt to next prompt)
42
+ current_context_tokens: int = 0 # Most recent input_tokens (current context size)
42
43
 
43
44
  @property
44
45
  def total_tokens(self) -> int:
@@ -249,13 +250,15 @@ def read_token_usage_from_session_file(
249
250
  since: Only count tokens from messages after this time
250
251
 
251
252
  Returns:
252
- Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
253
+ Dict with input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
254
+ and current_context_tokens (most recent input_tokens value)
253
255
  """
254
256
  totals = {
255
257
  "input_tokens": 0,
256
258
  "output_tokens": 0,
257
259
  "cache_creation_tokens": 0,
258
260
  "cache_read_tokens": 0,
261
+ "current_context_tokens": 0, # Most recent input_tokens
259
262
  }
260
263
 
261
264
  if not session_file.exists():
@@ -288,14 +291,18 @@ def read_token_usage_from_session_file(
288
291
  message = data.get("message", {})
289
292
  usage = message.get("usage", {})
290
293
  if usage:
291
- totals["input_tokens"] += usage.get("input_tokens", 0)
294
+ input_tokens = usage.get("input_tokens", 0)
295
+ cache_read = usage.get("cache_read_input_tokens", 0)
296
+ totals["input_tokens"] += input_tokens
292
297
  totals["output_tokens"] += usage.get("output_tokens", 0)
293
298
  totals["cache_creation_tokens"] += usage.get(
294
299
  "cache_creation_input_tokens", 0
295
300
  )
296
- totals["cache_read_tokens"] += usage.get(
297
- "cache_read_input_tokens", 0
298
- )
301
+ totals["cache_read_tokens"] += cache_read
302
+ # Track most recent context size (input + cached context)
303
+ context_size = input_tokens + cache_read
304
+ if context_size > 0:
305
+ totals["current_context_tokens"] = context_size
299
306
  except (json.JSONDecodeError, KeyError, TypeError):
300
307
  continue
301
308
  except IOError:
@@ -422,6 +429,7 @@ def get_session_stats(
422
429
  total_output = 0
423
430
  total_cache_creation = 0
424
431
  total_cache_read = 0
432
+ current_context = 0 # Track most recent context size
425
433
  all_work_times: List[float] = []
426
434
 
427
435
  for sid in session_ids:
@@ -433,6 +441,9 @@ def get_session_stats(
433
441
  total_output += usage["output_tokens"]
434
442
  total_cache_creation += usage["cache_creation_tokens"]
435
443
  total_cache_read += usage["cache_read_tokens"]
444
+ # Keep the largest current context (most recent across all session files)
445
+ if usage["current_context_tokens"] > current_context:
446
+ current_context = usage["current_context_tokens"]
436
447
 
437
448
  # Collect work times from this session file
438
449
  work_times = read_work_times_from_session_file(session_file, since=session_start)
@@ -445,4 +456,5 @@ def get_session_stats(
445
456
  cache_creation_tokens=total_cache_creation,
446
457
  cache_read_tokens=total_cache_read,
447
458
  work_times=all_work_times,
459
+ current_context_tokens=current_context,
448
460
  )
@@ -142,6 +142,17 @@ class RealTmux:
142
142
  def attach(self, session: str) -> None:
143
143
  os.execlp("tmux", "tmux", "attach-session", "-t", session)
144
144
 
145
+ def select_window(self, session: str, window: int) -> bool:
146
+ """Select a window in a tmux session (for external pane sync)."""
147
+ try:
148
+ result = subprocess.run(
149
+ ["tmux", "select-window", "-t", f"{session}:{window}"],
150
+ capture_output=True, timeout=5
151
+ )
152
+ return result.returncode == 0
153
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
154
+ return False
155
+
145
156
 
146
157
  class RealFileSystem:
147
158
  """Production implementation of FileSystemInterface"""
@@ -4,6 +4,9 @@ Launcher for interactive Claude Code sessions in tmux windows.
4
4
  All Claude sessions launched by overcode are interactive - users can
5
5
  take over at any time. Initial prompts are sent as keystrokes after
6
6
  Claude starts, not as CLI arguments.
7
+
8
+ TODO: Extract _send_prompt_to_window to a shared tmux utilities module
9
+ (duplicated in supervisor_daemon.py)
7
10
  """
8
11
 
9
12
  import time
@@ -77,6 +77,10 @@ class MockTmux:
77
77
  def attach(self, session: str) -> None:
78
78
  pass # No-op in tests
79
79
 
80
+ def select_window(self, session: str, window: int) -> bool:
81
+ """Select a window - no-op in tests, just return True."""
82
+ return session in self.sessions
83
+
80
84
 
81
85
  class MockFileSystem:
82
86
  """Mock implementation of FileSystemInterface for testing"""