overcode 0.3.0__tar.gz → 0.3.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 (119) hide show
  1. {overcode-0.3.0/src/overcode.egg-info → overcode-0.3.2}/PKG-INFO +1 -1
  2. {overcode-0.3.0 → overcode-0.3.2}/pyproject.toml +1 -1
  3. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/bundled_skills.py +44 -0
  4. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/__init__.py +1 -0
  5. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/_shared.py +8 -0
  6. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/agent.py +8 -0
  7. overcode-0.3.2/src/overcode/cli/jobs.py +156 -0
  8. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/monitoring.py +4 -1
  9. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/split.py +51 -24
  10. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/config.py +13 -0
  11. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/history_reader.py +22 -0
  12. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/implementations.py +37 -0
  13. overcode-0.3.2/src/overcode/job_launcher.py +226 -0
  14. overcode-0.3.2/src/overcode/job_manager.py +351 -0
  15. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/launcher.py +8 -1
  16. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/mocks.py +4 -0
  17. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/monitor_daemon.py +13 -7
  18. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/monitor_daemon_core.py +8 -8
  19. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/monitor_daemon_state.py +4 -0
  20. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/protocols.py +8 -0
  21. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/session_manager.py +5 -0
  22. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/settings.py +70 -13
  23. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/sister_poller.py +2 -0
  24. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_detector.py +18 -6
  25. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_patterns.py +83 -0
  26. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summary_columns.py +27 -6
  27. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/supervisor_layout.sh +10 -5
  28. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tmux_manager.py +17 -0
  29. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tmux_utils.py +22 -0
  30. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui.py +400 -114
  31. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui.tcss +43 -16
  32. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/input.py +4 -1
  33. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/navigation.py +14 -2
  34. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/session.py +14 -9
  35. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/view.py +57 -46
  36. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_render.py +6 -9
  37. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/__init__.py +2 -0
  38. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/help_overlay.py +4 -6
  39. overcode-0.3.2/src/overcode/tui_widgets/job_summary.py +129 -0
  40. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/preview_pane.py +20 -0
  41. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/session_summary.py +14 -112
  42. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_api.py +1 -0
  43. {overcode-0.3.0 → overcode-0.3.2/src/overcode.egg-info}/PKG-INFO +1 -1
  44. {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/SOURCES.txt +4 -0
  45. {overcode-0.3.0 → overcode-0.3.2}/LICENSE +0 -0
  46. {overcode-0.3.0 → overcode-0.3.2}/MANIFEST.in +0 -0
  47. {overcode-0.3.0 → overcode-0.3.2}/README.md +0 -0
  48. {overcode-0.3.0 → overcode-0.3.2}/setup.cfg +0 -0
  49. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/__init__.py +0 -0
  50. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/agent_scanner.py +0 -0
  51. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/claude_config.py +0 -0
  52. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/claude_pid.py +0 -0
  53. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/__main__.py +0 -0
  54. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/budget.py +0 -0
  55. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/config.py +0 -0
  56. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/daemon.py +0 -0
  57. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/hooks.py +0 -0
  58. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/perms.py +0 -0
  59. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/sister.py +0 -0
  60. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/skills.py +0 -0
  61. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/daemon_claude_skill.md +0 -0
  62. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/daemon_logging.py +0 -0
  63. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/daemon_utils.py +0 -0
  64. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/data_export.py +0 -0
  65. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/dependency_check.py +0 -0
  66. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/duration.py +0 -0
  67. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/exceptions.py +0 -0
  68. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/follow_mode.py +0 -0
  69. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/hook_handler.py +0 -0
  70. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/hook_status_detector.py +0 -0
  71. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/interfaces.py +0 -0
  72. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/logging_config.py +0 -0
  73. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/notifier.py +0 -0
  74. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/pid_utils.py +0 -0
  75. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/presence_logger.py +0 -0
  76. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/sister_controller.py +0 -0
  77. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/standing_instructions.py +0 -0
  78. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_constants.py +0 -0
  79. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_detector_factory.py +0 -0
  80. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_history.py +0 -0
  81. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summarizer_client.py +0 -0
  82. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summarizer_component.py +0 -0
  83. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summary_groups.py +0 -0
  84. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/supervisor_daemon.py +0 -0
  85. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/supervisor_daemon_core.py +0 -0
  86. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/__init__.py +0 -0
  87. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/renderer.py +0 -0
  88. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/tmux_driver.py +0 -0
  89. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/tui_eye.py +0 -0
  90. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/tui_eye_skill.md +0 -0
  91. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/time_context.py +0 -0
  92. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/__init__.py +0 -0
  93. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/daemon.py +0 -0
  94. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_helpers.py +0 -0
  95. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_logic.py +0 -0
  96. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  97. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/command_bar.py +0 -0
  98. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  99. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  100. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  101. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  102. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  103. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  104. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/status_timeline.py +0 -0
  105. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  106. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/usage_monitor.py +0 -0
  107. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web/__init__.py +0 -0
  108. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web/templates/analytics.html +0 -0
  109. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web/templates/dashboard.html +0 -0
  110. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_chartjs.py +0 -0
  111. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_control_api.py +0 -0
  112. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_server.py +0 -0
  113. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_server_runner.py +0 -0
  114. {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_templates.py +0 -0
  115. {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/dependency_links.txt +0 -0
  116. {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/entry_points.txt +0 -0
  117. {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/requires.txt +0 -0
  118. {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/top_level.txt +0 -0
  119. {overcode-0.3.0 → overcode-0.3.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.3.0
3
+ Version: 0.3.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overcode"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -82,6 +82,26 @@ overcode send my-agent escape # Reject permission
82
82
  overcode send my-agent "yes" # Send text response
83
83
  ```
84
84
 
85
+ ## Jobs (Long-Running Bash Commands)
86
+
87
+ For long-running commands (10min+ \u2014 test suites, builds, deploys), use `overcode bash` to launch them as tracked jobs in a separate tmux session. This keeps the output visible and lets you monitor multiple concurrent jobs from the TUI.
88
+
89
+ ```bash
90
+ # Launch a job
91
+ overcode bash "pytest tests/ -x" --name unit-tests
92
+ overcode bash "npm run build" -d ~/frontend
93
+ overcode bash "make deploy-staging" --agent my-agent # Link to an agent
94
+
95
+ # Manage jobs
96
+ overcode jobs list [--all] # List running (or all) jobs
97
+ overcode jobs kill <name> # Kill a running job
98
+ overcode jobs attach <name> # Attach to job's tmux window
99
+ overcode jobs clear # Remove completed/failed/killed jobs
100
+
101
+ # TUI: press J to toggle jobs view, j/k to navigate, x to kill, c to clear
102
+ overcode monitor --jobs # Start TUI directly in jobs view
103
+ ```
104
+
85
105
  ## Standing Instructions Presets
86
106
 
87
107
  `overcode instruct <name> <preset>` \u2014 available presets: `DO_NOTHING`, `STANDARD`, `PERMISSIVE`, `CAUTIOUS`, `RESEARCH`, `CODING`, `TESTING`, `REVIEW`, `DEPLOY`, `AUTONOMOUS`, `MINIMAL`.
@@ -194,6 +214,30 @@ Pass arbitrary Claude CLI flags with `--claude-arg` (repeatable):
194
214
  overcode launch -n fast --claude-arg "--model haiku" --claude-arg "--effort low" -p "Quick review"
195
215
  ```
196
216
 
217
+ ## Long-Running Shell Commands (Jobs)
218
+
219
+ When a task involves a long-running shell command (10min+ \u2014 full test suites, builds, deploys, docker compose), use `overcode bash` instead of running it inline. This launches the command as a tracked job in a separate tmux session with its own window, so you can monitor it without blocking your agent session.
220
+
221
+ ```bash
222
+ # Launch a test suite as a tracked job
223
+ overcode bash "pytest tests/ -x --timeout=600" --name full-tests
224
+
225
+ # Launch a build linked to the current agent
226
+ overcode bash "npm run build" --name frontend-build --agent my-agent
227
+
228
+ # Check on it later
229
+ overcode jobs list
230
+ overcode jobs attach full-tests # Attach to see live output
231
+ overcode jobs kill full-tests # Kill if needed
232
+ ```
233
+
234
+ **When to use `overcode bash` vs running inline:**
235
+ - Inline (`Bash` tool): Quick commands (<2 min) where you need the result to continue
236
+ - `overcode bash`: Long commands (10min+) \u2014 test suites, builds, deploys, docker operations
237
+ - `overcode launch`: When you need a full Claude Code agent with AI reasoning
238
+
239
+ Jobs are visible in the TUI jobs view (press `J`) and auto-clean after 24h (configurable via `jobs.retention_hours` in config).
240
+
197
241
  ## Rules
198
242
 
199
243
  - Parent auto-detected from `OVERCODE_SESSION_NAME` \u2014 no `--parent` needed inside an overcode agent
@@ -22,6 +22,7 @@ from . import daemon # noqa: F401
22
22
  from . import sister # noqa: F401
23
23
  from . import config # noqa: F401
24
24
  from . import split # noqa: F401
25
+ from . import jobs # noqa: F401
25
26
 
26
27
 
27
28
  def main():
@@ -76,6 +76,14 @@ sister_app = typer.Typer(
76
76
  )
77
77
  app.add_typer(sister_app, name="sister")
78
78
 
79
+ # Jobs subcommand group
80
+ jobs_app = typer.Typer(
81
+ name="jobs",
82
+ help="Manage overcode jobs.",
83
+ no_args_is_help=True,
84
+ )
85
+ app.add_typer(jobs_app, name="jobs")
86
+
79
87
  # Config subcommand group
80
88
  config_app = typer.Typer(
81
89
  name="config",
@@ -192,6 +192,10 @@ def launch(
192
192
  Optional[str],
193
193
  typer.Option("--allowed-tools", help="Comma-separated tools for Claude (e.g. 'Bash,Read,Write,Edit')"),
194
194
  ] = None,
195
+ model: Annotated[
196
+ Optional[str],
197
+ typer.Option("--model", "-m", help="Claude model (e.g. sonnet, opus, haiku, or full name)"),
198
+ ] = None,
195
199
  claude_args: Annotated[
196
200
  Optional[List[str]],
197
201
  typer.Option("--claude-arg", help="Extra Claude CLI flag (repeatable, e.g. '--model haiku')"),
@@ -283,6 +287,7 @@ def launch(
283
287
  agent_teams=teams,
284
288
  budget_usd=budget,
285
289
  claude_agent=agent,
290
+ model=model,
286
291
  )
287
292
 
288
293
  if result:
@@ -536,6 +541,9 @@ def list_agents(
536
541
  claude_stats = get_session_stats(sess)
537
542
  except Exception:
538
543
  pass
544
+ if claude_stats is None and getattr(sess, 'is_remote', False):
545
+ from ..history_reader import synthesize_remote_stats
546
+ claude_stats = synthesize_remote_stats(sess)
539
547
 
540
548
  git_diff = None
541
549
  if getattr(sess, 'is_remote', False):
@@ -0,0 +1,156 @@
1
+ """
2
+ CLI commands for managing overcode jobs.
3
+ """
4
+
5
+ from typing import Annotated, Optional
6
+
7
+ import typer
8
+ from rich import print as rprint
9
+
10
+ from ._shared import app, jobs_app
11
+
12
+
13
+ @app.command()
14
+ def bash(
15
+ command: Annotated[str, typer.Argument(help="Bash command to run as a tracked job")],
16
+ name: Annotated[Optional[str], typer.Option("--name", "-n", help="Job name (auto-derived if omitted)")] = None,
17
+ directory: Annotated[str, typer.Option("--directory", "-d", help="Working directory")] = ".",
18
+ agent: Annotated[Optional[str], typer.Option("--agent", "-a", help="Link to an agent session by name")] = None,
19
+ follow: Annotated[bool, typer.Option("--follow", "-f", help="Attach to the job's tmux window after launch")] = True,
20
+ ):
21
+ """Launch a bash command as a tracked job."""
22
+ import os
23
+ from ..job_launcher import JobLauncher
24
+ from ..session_manager import SessionManager
25
+
26
+ directory = os.path.abspath(directory)
27
+
28
+ agent_session_id = None
29
+ agent_name = None
30
+ # Auto-detect calling agent from env if --agent not specified
31
+ if not agent:
32
+ agent = os.environ.get("OVERCODE_SESSION_NAME")
33
+ if agent:
34
+ sm = SessionManager()
35
+ sess = sm.get_session_by_name(agent)
36
+ if sess:
37
+ agent_session_id = sess.id
38
+ agent_name = sess.name
39
+ else:
40
+ rprint(f"[yellow]Warning: Agent '{agent}' not found, launching without link[/yellow]")
41
+
42
+ launcher = JobLauncher()
43
+ job = launcher.launch(
44
+ command=command,
45
+ name=name,
46
+ directory=directory,
47
+ agent_session_id=agent_session_id,
48
+ agent_name=agent_name,
49
+ )
50
+
51
+ rprint(f"[green]✓[/green] Job '[bold]{job.name}[/bold]' launched")
52
+ rprint(f" Command: {job.command}")
53
+ rprint(f" Window: {job.tmux_session}:{job.tmux_window}")
54
+ if agent_name:
55
+ rprint(f" Linked to agent: {agent_name}")
56
+
57
+ if follow:
58
+ launcher.attach(job.name)
59
+
60
+
61
+ @jobs_app.command("list")
62
+ def list_jobs(
63
+ all: Annotated[bool, typer.Option("--all", "-a", help="Include completed/failed/killed jobs")] = False,
64
+ ):
65
+ """List tracked jobs."""
66
+ from ..job_launcher import JobLauncher
67
+
68
+ launcher = JobLauncher()
69
+ jobs = launcher.list_jobs(include_completed=all)
70
+
71
+ if not jobs:
72
+ rprint("[dim]No jobs running[/dim]" if not all else "[dim]No jobs found[/dim]")
73
+ return
74
+
75
+ for job in sorted(jobs, key=lambda j: j.start_time, reverse=True):
76
+ status_icon = {
77
+ "running": "[green]●[/green]",
78
+ "completed": "[green]✓[/green]",
79
+ "failed": "[red]✗[/red]",
80
+ "killed": "[yellow]✗[/yellow]",
81
+ }.get(job.status, "?")
82
+
83
+ exit_str = ""
84
+ if job.exit_code is not None:
85
+ exit_str = f" ({job.exit_code})"
86
+
87
+ duration = ""
88
+ if job.start_time:
89
+ from datetime import datetime
90
+ try:
91
+ start = datetime.fromisoformat(job.start_time)
92
+ end = datetime.fromisoformat(job.end_time) if job.end_time else datetime.now()
93
+ dur_sec = (end - start).total_seconds()
94
+ mins, secs = divmod(int(dur_sec), 60)
95
+ duration = f"{mins}m{secs:02d}s"
96
+ except ValueError:
97
+ pass
98
+
99
+ agent_str = f" ← {job.agent_name}" if job.agent_name else ""
100
+ rprint(
101
+ f" {status_icon} {job.name:<16} {job.command:<30} {duration:>8} "
102
+ f"{job.status}{exit_str}{agent_str}"
103
+ )
104
+
105
+
106
+ @jobs_app.command("kill")
107
+ def kill_job(
108
+ name: Annotated[str, typer.Argument(help="Job name to kill")],
109
+ ):
110
+ """Kill a running job."""
111
+ from ..job_launcher import JobLauncher
112
+
113
+ launcher = JobLauncher()
114
+ if launcher.kill_job(name):
115
+ rprint(f"[green]✓[/green] Job '[bold]{name}[/bold]' killed")
116
+ else:
117
+ rprint(f"[red]✗[/red] Could not kill job '{name}' (not found or not running)")
118
+ raise typer.Exit(1)
119
+
120
+
121
+ @jobs_app.command("attach")
122
+ def attach_job(
123
+ name: Annotated[str, typer.Argument(help="Job name to attach to")],
124
+ bare: Annotated[bool, typer.Option("--bare", "-b", help="Attach with stripped tmux chrome")] = False,
125
+ ):
126
+ """Attach to a job's tmux window."""
127
+ from ..job_launcher import JobLauncher
128
+
129
+ launcher = JobLauncher()
130
+ try:
131
+ launcher.attach(name, bare=bare)
132
+ except ValueError as e:
133
+ rprint(f"[red]✗[/red] {e}")
134
+ raise typer.Exit(1)
135
+
136
+
137
+ @jobs_app.command("clear")
138
+ def clear_completed():
139
+ """Remove all completed/failed/killed jobs."""
140
+ from ..job_manager import JobManager
141
+
142
+ manager = JobManager()
143
+ manager.clear_completed()
144
+ rprint("[green]✓[/green] Cleared completed jobs")
145
+
146
+
147
+ @jobs_app.command("_complete", hidden=True)
148
+ def mark_complete(
149
+ job_id: Annotated[str, typer.Argument(help="Job ID")],
150
+ exit_code: Annotated[int, typer.Argument(help="Exit code")],
151
+ ):
152
+ """Internal: mark a job as complete (called by wrapper script)."""
153
+ from ..job_manager import JobManager
154
+
155
+ manager = JobManager()
156
+ manager.mark_complete(job_id, exit_code)
@@ -273,6 +273,9 @@ def monitor(
273
273
  sync_target: Annotated[
274
274
  str, typer.Option("--sync-target", hidden=True, help="Linked tmux session for pane sync (set by `overcode split`)")
275
275
  ] = "",
276
+ jobs: Annotated[
277
+ bool, typer.Option("--jobs", help="Start in jobs view")
278
+ ] = False,
276
279
  ):
277
280
  """Launch the standalone TUI monitor."""
278
281
  if restart:
@@ -299,7 +302,7 @@ def monitor(
299
302
 
300
303
  from ..tui import run_tui
301
304
 
302
- run_tui(session, diagnostics=diagnostics, sync_target=sync_target or None)
305
+ run_tui(session, diagnostics=diagnostics, sync_target=sync_target or None, initial_jobs_mode=jobs)
303
306
 
304
307
 
305
308
  @app.command()
@@ -34,6 +34,7 @@ from typing import Annotated
34
34
  import typer
35
35
 
36
36
  from ._shared import app, SessionOption
37
+ from ..tmux_utils import get_pane_base_index
37
38
 
38
39
 
39
40
  def _acquire_setup_lock() -> bool:
@@ -179,11 +180,13 @@ def _setup_linked_session(agents_session: str) -> str:
179
180
 
180
181
  def _are_keybindings_installed() -> bool:
181
182
  """Check if overcode keybindings are already installed."""
182
- # Check for our Tab binding as a sentinel
183
+ # Check for our M-j binding as a sentinel — look for the exact
184
+ # send-keys target rather than a substring match on the window name,
185
+ # to avoid false positives from unrelated bindings (#384).
183
186
  result = _tmux("list-keys", "-T", "root")
184
187
  if result.returncode != 0:
185
188
  return False
186
- return SPLIT_WINDOW_NAME in result.stdout
189
+ return f"send-keys -t overcode:{SPLIT_WINDOW_NAME}." in result.stdout
187
190
 
188
191
 
189
192
  def _remove_keybindings() -> None:
@@ -191,7 +194,7 @@ def _remove_keybindings() -> None:
191
194
  from ..config import get_tmux_toggle_key
192
195
 
193
196
  toggle_key = get_tmux_toggle_key() or DEFAULT_TOGGLE_KEY
194
- keys = [toggle_key, "M-j", "M-k", "PPage", "NPage", "WheelUpPane", "WheelDownPane"]
197
+ keys = [toggle_key, "M-j", "M-k", "M-b", "PPage", "NPage", "WheelUpPane", "WheelDownPane"]
195
198
  # Also unbind Tab if toggle_key changed away from it (stale binding)
196
199
  if toggle_key != "Tab":
197
200
  keys.append("Tab")
@@ -232,22 +235,30 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
232
235
  # --- Agent navigation from the bottom (terminal) pane ---
233
236
  # Option+J / Option+K (Meta+j/k) cycle agents by sending j/k
234
237
  # to the monitor pane, which navigates and syncs the terminal.
238
+ _base = get_pane_base_index()
235
239
  _in_bottom = (
236
240
  f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},"
237
- f"#{{!=:#{{pane_index}},0}}}}"
241
+ f"#{{!=:#{{pane_index}},{_base}}}}}"
238
242
  )
239
243
  _tmux(
240
244
  "bind-key", "-n", "M-j",
241
245
  "if-shell", "-F", _in_bottom,
242
- f"send-keys -t overcode:{SPLIT_WINDOW_NAME}.0 j",
246
+ f"send-keys -t overcode:{SPLIT_WINDOW_NAME}.{_base} j",
243
247
  "send-keys M-j",
244
248
  )
245
249
  _tmux(
246
250
  "bind-key", "-n", "M-k",
247
251
  "if-shell", "-F", _in_bottom,
248
- f"send-keys -t overcode:{SPLIT_WINDOW_NAME}.0 k",
252
+ f"send-keys -t overcode:{SPLIT_WINDOW_NAME}.{_base} k",
249
253
  "send-keys M-k",
250
254
  )
255
+ # Option+B: navigate to bell (next agent needing attention)
256
+ _tmux(
257
+ "bind-key", "-n", "M-b",
258
+ "if-shell", "-F", _in_bottom,
259
+ f"send-keys -t overcode:{SPLIT_WINDOW_NAME}.{_base} b",
260
+ "send-keys M-b",
261
+ )
251
262
 
252
263
  # --- Scrollback for the nested tmux in the bottom pane ---
253
264
  # The bottom pane runs a nested tmux client. The outer tmux
@@ -256,12 +267,12 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
256
267
  # copy mode in the inner session via the tmux API.
257
268
  if linked_session:
258
269
  # PageUp: enter copy mode + scroll up, but only when in the
259
- # bottom pane (pane_index != 0) of the split window.
270
+ # bottom pane (pane_index != base) of the split window.
260
271
  # Top pane (Textual TUI) and other windows get normal PageUp.
261
272
  _tmux(
262
273
  "bind-key", "-n", "PPage",
263
274
  "if-shell", "-F",
264
- f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},0}}}}",
275
+ f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},{_base}}}}}",
265
276
  f"copy-mode -t {linked_session} -u",
266
277
  "send-keys PPage",
267
278
  )
@@ -269,7 +280,7 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
269
280
  _tmux(
270
281
  "bind-key", "-n", "NPage",
271
282
  "if-shell", "-F",
272
- f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},0}}}}",
283
+ f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},{_base}}}}}",
273
284
  f"send-keys -t {linked_session} NPage",
274
285
  "send-keys NPage",
275
286
  )
@@ -281,7 +292,7 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
281
292
  # already active), then send scroll-up/down commands to it.
282
293
  _in_bottom = (
283
294
  f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},"
284
- f"#{{!=:#{{pane_index}},0}}}}"
295
+ f"#{{!=:#{{pane_index}},{_base}}}}}"
285
296
  )
286
297
  _tmux(
287
298
  "bind-key", "-n", "WheelUpPane",
@@ -403,7 +414,7 @@ def tmux_resize():
403
414
 
404
415
  # Apply the new ratio
405
416
  new_height = max(int(int(info.split(":")[1]) * next_ratio / 100), 5)
406
- _tmux("resize-pane", "-t", f"{target}.0", "-y", str(new_height))
417
+ _tmux("resize-pane", "-t", f"{target}.{get_pane_base_index()}", "-y", str(new_height))
407
418
 
408
419
 
409
420
  @app.command("tmux")
@@ -594,20 +605,30 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
594
605
  if existing:
595
606
  if _is_split_window_healthy(existing):
596
607
  if in_tmux:
597
- # Check if a real (non-tiny) client is already on overcode.
608
+ # Check if a real (non-nested) client is already on overcode.
598
609
  # If so, no switch needed — the user is already there or
599
610
  # another terminal has it open. Switching blindly can
600
611
  # accidentally move the bottom pane's nested client from
601
612
  # oc-view-agents to overcode, creating a recursive display
602
613
  # that collapses the window.
614
+ #
615
+ # Detect nested clients by comparing client TTYs against
616
+ # pane TTYs in the split window (#387). A nested tmux
617
+ # client's TTY matches one of the pane TTYs.
618
+ pane_ttys = set(
619
+ _tmux_output(
620
+ "list-panes", "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}",
621
+ "-F", "#{pane_tty}",
622
+ ).splitlines()
623
+ )
603
624
  oc_clients = _tmux_output(
604
625
  "list-clients", "-t", oc_session,
605
- "-F", "#{client_height}",
626
+ "-F", "#{client_tty}",
606
627
  )
607
628
  has_real_client = any(
608
- int(h) > 10
609
- for h in oc_clients.splitlines()
610
- if h.strip().isdigit()
629
+ tty.strip() not in pane_ttys
630
+ for tty in oc_clients.splitlines()
631
+ if tty.strip()
611
632
  )
612
633
  if not has_real_client:
613
634
  # No real client on overcode yet — switch the caller's
@@ -625,14 +646,14 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
625
646
  else:
626
647
  # Respawn before attach — execlp replaces this process
627
648
  _tmux("respawn-pane", "-k",
628
- "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.0",
649
+ "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}",
629
650
  monitor_cmd)
630
651
  rprint(f"[green]Attaching to existing {SPLIT_WINDOW_NAME} window (monitor restarted)...[/green]")
631
652
  time.sleep(0.2)
632
653
  os.execlp("tmux", "tmux", "attach-session", "-t", oc_session)
633
654
  # Always restart the monitor so code changes take effect
634
655
  _tmux("respawn-pane", "-k",
635
- "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.0",
656
+ "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}",
636
657
  monitor_cmd)
637
658
  rprint(f"[green]Switched to existing {SPLIT_WINDOW_NAME} window (monitor restarted)[/green]")
638
659
  return
@@ -661,7 +682,7 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
661
682
 
662
683
  # The bottom pane runs a nested tmux attach. We must unset $TMUX
663
684
  # because tmux refuses to attach from inside an existing session.
664
- attach_cmd = f"unset TMUX; tmux attach-session -t {linked}"
685
+ attach_cmd = f"sh -c 'unset TMUX; exec tmux attach-session -t {linked}'"
665
686
 
666
687
  # If an "overcode" session exists but has no split window, it might be:
667
688
  # (a) a stale session from a previous overcode run, or
@@ -742,7 +763,7 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
742
763
  result = _tmux(
743
764
  "split-window", "-v",
744
765
  "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}",
745
- "-p", str(100 - ratio),
766
+ "-l", f"{100 - ratio}%",
746
767
  attach_cmd,
747
768
  )
748
769
  if result.returncode != 0:
@@ -750,17 +771,23 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
750
771
  raise typer.Exit(1)
751
772
 
752
773
  # Focus top pane
753
- _tmux("select-pane", "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.0")
774
+ _tmux("select-pane", "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}")
754
775
 
755
776
  rprint(f"[green]Split layout ready.[/green] Tab toggles panes.")
756
777
 
757
778
  else:
758
779
  # --- Outside tmux: create detached, split, then attach ---
759
780
  if need_new_session:
781
+ # Use actual terminal size (fall back to 200x50 if unavailable)
782
+ try:
783
+ term_size = os.get_terminal_size()
784
+ term_x, term_y = str(term_size.columns), str(term_size.lines)
785
+ except OSError:
786
+ term_x, term_y = "200", "50"
760
787
  result = _tmux(
761
788
  "new-session", "-d", "-s", oc_session,
762
789
  "-n", SPLIT_WINDOW_NAME,
763
- "-x", "200", "-y", "50",
790
+ "-x", term_x, "-y", term_y,
764
791
  monitor_cmd,
765
792
  )
766
793
  if result.returncode != 0:
@@ -785,12 +812,12 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
785
812
  _tmux(
786
813
  "split-window", "-v",
787
814
  "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}",
788
- "-p", str(100 - ratio),
815
+ "-l", f"{100 - ratio}%",
789
816
  attach_cmd,
790
817
  )
791
818
 
792
819
  # Focus top pane
793
- _tmux("select-pane", "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.0")
820
+ _tmux("select-pane", "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}")
794
821
 
795
822
  # Attach to the session (replaces this process)
796
823
  rprint(f"[green]Attaching to split layout...[/green]")
@@ -342,6 +342,19 @@ def save_new_agent_defaults(defaults: dict) -> None:
342
342
  save_config(config)
343
343
 
344
344
 
345
+ def get_jobs_retention_hours() -> float:
346
+ """Get job retention period in hours.
347
+
348
+ Config format in ~/.overcode/config.yaml:
349
+ jobs:
350
+ retention_hours: 24
351
+
352
+ Returns:
353
+ Retention hours (default 24)
354
+ """
355
+ return float(_get_config_value("jobs.retention_hours", 24))
356
+
357
+
345
358
  def get_sisters_config() -> List[dict]:
346
359
  """Get sister instance configuration for cross-machine monitoring.
347
360
 
@@ -99,6 +99,28 @@ class ClaudeSessionStats:
99
99
  return sorted_times[n // 2]
100
100
 
101
101
 
102
+ def synthesize_remote_stats(session) -> "ClaudeSessionStats":
103
+ """Synthesize ClaudeSessionStats for a remote session from daemon_state.
104
+
105
+ Remote sessions carry a remote_daemon_state dict with all
106
+ SessionDaemonState fields. Extract what we need so that render
107
+ columns (cost, tokens, context %, model) display correctly.
108
+ """
109
+ rds = getattr(session, 'remote_daemon_state', None) or {}
110
+ stats = session.stats
111
+ mwt = getattr(session, 'remote_median_work_time', None) or rds.get('median_work_time', 0.0)
112
+ return ClaudeSessionStats(
113
+ interaction_count=stats.interaction_count,
114
+ input_tokens=rds.get('input_tokens', stats.total_tokens),
115
+ output_tokens=rds.get('output_tokens', 0),
116
+ cache_creation_tokens=rds.get('cache_creation_tokens', 0),
117
+ cache_read_tokens=rds.get('cache_read_tokens', 0),
118
+ work_times=[mwt] if mwt > 0 else [],
119
+ current_context_tokens=rds.get('current_context_tokens', 0),
120
+ model=rds.get('model'),
121
+ )
122
+
123
+
102
124
  @dataclass
103
125
  class HistoryEntry:
104
126
  """A single interaction from Claude Code history."""
@@ -235,6 +235,16 @@ class RealTmux:
235
235
  from .tmux_utils import attach_bare
236
236
  attach_bare(session, window)
237
237
 
238
+ def get_pane_pid(self, session: str, window: str) -> Optional[int]:
239
+ """Get the PID of the shell process in a window's first pane."""
240
+ try:
241
+ pane = self._get_pane(session, window)
242
+ if pane is None:
243
+ return None
244
+ return int(pane.pane_pid)
245
+ except (LibTmuxException, ValueError, TypeError):
246
+ return None
247
+
238
248
  def select_window(self, session: str, window: str) -> bool:
239
249
  """Select a window in a tmux session (for external pane sync)."""
240
250
  try:
@@ -246,6 +256,33 @@ class RealTmux:
246
256
  except LibTmuxException:
247
257
  return False
248
258
 
259
+ def resize_window(self, session: str, window: str, width: int, height: int) -> bool:
260
+ """Resize a tmux window to match terminal dimensions.
261
+
262
+ Used to fix window size mismatches when switching between sessions
263
+ after terminal resize (#245).
264
+
265
+ Args:
266
+ session: Session name
267
+ window: Window name/index
268
+ width: Target width in columns
269
+ height: Target height in rows
270
+
271
+ Returns:
272
+ True if resize succeeded, False otherwise
273
+ """
274
+ try:
275
+ # Use tmux resize-window command
276
+ # Format: session:window.pane
277
+ target = f"{session}:{window}"
278
+ result = self.run(
279
+ ["tmux", "resize-window", "-t", target, "-x", str(width), "-y", str(height)],
280
+ timeout=2
281
+ )
282
+ return result is not None and result.get("returncode") == 0
283
+ except Exception:
284
+ return False
285
+
249
286
 
250
287
  class RealFileSystem:
251
288
  """Production implementation of FileSystemInterface"""