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.
- {overcode-0.3.0/src/overcode.egg-info → overcode-0.3.2}/PKG-INFO +1 -1
- {overcode-0.3.0 → overcode-0.3.2}/pyproject.toml +1 -1
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/bundled_skills.py +44 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/__init__.py +1 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/_shared.py +8 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/agent.py +8 -0
- overcode-0.3.2/src/overcode/cli/jobs.py +156 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/monitoring.py +4 -1
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/split.py +51 -24
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/config.py +13 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/history_reader.py +22 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/implementations.py +37 -0
- overcode-0.3.2/src/overcode/job_launcher.py +226 -0
- overcode-0.3.2/src/overcode/job_manager.py +351 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/launcher.py +8 -1
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/mocks.py +4 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/monitor_daemon.py +13 -7
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/monitor_daemon_core.py +8 -8
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/monitor_daemon_state.py +4 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/protocols.py +8 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/session_manager.py +5 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/settings.py +70 -13
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/sister_poller.py +2 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_detector.py +18 -6
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_patterns.py +83 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summary_columns.py +27 -6
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/supervisor_layout.sh +10 -5
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tmux_manager.py +17 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tmux_utils.py +22 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui.py +400 -114
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui.tcss +43 -16
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/input.py +4 -1
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/navigation.py +14 -2
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/session.py +14 -9
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/view.py +57 -46
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_render.py +6 -9
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/__init__.py +2 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/help_overlay.py +4 -6
- overcode-0.3.2/src/overcode/tui_widgets/job_summary.py +129 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/preview_pane.py +20 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/session_summary.py +14 -112
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_api.py +1 -0
- {overcode-0.3.0 → overcode-0.3.2/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/SOURCES.txt +4 -0
- {overcode-0.3.0 → overcode-0.3.2}/LICENSE +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/MANIFEST.in +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/README.md +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/setup.cfg +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/__init__.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/claude_config.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/claude_pid.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/budget.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/config.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/perms.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/sister.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/cli/skills.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/data_export.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/dependency_check.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/duration.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/exceptions.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/follow_mode.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/hook_handler.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/hook_status_detector.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/interfaces.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/logging_config.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/notifier.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/pid_utils.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/presence_logger.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/sister_controller.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_constants.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/status_history.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/summary_groups.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/time_context.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_logic.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/command_bar.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web/__init__.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_control_api.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_server.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode/web_templates.py +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.3.0 → overcode-0.3.2}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -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
|
|
@@ -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
|
|
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}},
|
|
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}.
|
|
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}.
|
|
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 !=
|
|
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}},
|
|
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}},
|
|
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}},
|
|
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}.
|
|
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-
|
|
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", "#{
|
|
626
|
+
"-F", "#{client_tty}",
|
|
606
627
|
)
|
|
607
628
|
has_real_client = any(
|
|
608
|
-
|
|
609
|
-
for
|
|
610
|
-
if
|
|
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}.
|
|
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}.
|
|
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
|
-
"-
|
|
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}.
|
|
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",
|
|
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
|
-
"-
|
|
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}.
|
|
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"""
|