overcode 0.3.1__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.1/src/overcode.egg-info → overcode-0.3.2}/PKG-INFO +1 -1
- {overcode-0.3.1 → overcode-0.3.2}/pyproject.toml +1 -1
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/split.py +44 -24
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/job_launcher.py +18 -3
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/settings.py +1 -1
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_detector.py +11 -4
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/supervisor_layout.sh +10 -5
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tmux_manager.py +1 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tmux_utils.py +22 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui.py +17 -6
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/session.py +4 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/view.py +1 -1
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/help_overlay.py +1 -1
- {overcode-0.3.1 → overcode-0.3.2/src/overcode.egg-info}/PKG-INFO +1 -1
- {overcode-0.3.1 → overcode-0.3.2}/LICENSE +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/MANIFEST.in +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/README.md +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/setup.cfg +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/__init__.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/agent_scanner.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/bundled_skills.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/claude_config.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/claude_pid.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/__init__.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/__main__.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/_shared.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/agent.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/budget.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/config.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/daemon.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/hooks.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/jobs.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/monitoring.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/perms.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/sister.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/skills.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/config.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/daemon_claude_skill.md +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/daemon_logging.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/daemon_utils.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/data_export.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/dependency_check.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/duration.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/exceptions.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/follow_mode.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/history_reader.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/hook_handler.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/hook_status_detector.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/implementations.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/interfaces.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/job_manager.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/launcher.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/logging_config.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/mocks.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/monitor_daemon.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/monitor_daemon_core.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/monitor_daemon_state.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/notifier.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/pid_utils.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/presence_logger.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/protocols.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/session_manager.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/sister_controller.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/sister_poller.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/standing_instructions.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_constants.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_detector_factory.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_history.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_patterns.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summarizer_client.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summarizer_component.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summary_columns.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summary_groups.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/supervisor_daemon.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/supervisor_daemon_core.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/__init__.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/renderer.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/tmux_driver.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/tui_eye.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/tui_eye_skill.md +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/time_context.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui.tcss +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/__init__.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/daemon.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/input.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/navigation.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_helpers.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_logic.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_render.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/__init__.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/command_bar.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_panel.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/job_summary.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/preview_pane.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/session_summary.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/status_timeline.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/usage_monitor.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web/__init__.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web/templates/analytics.html +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web/templates/dashboard.html +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_api.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_chartjs.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_control_api.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_server.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_server_runner.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_templates.py +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/SOURCES.txt +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/dependency_links.txt +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/entry_points.txt +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/requires.txt +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/top_level.txt +0 -0
- {overcode-0.3.1 → overcode-0.3.2}/tests/test_e2e_multi_agent_jokes.py +0 -0
|
@@ -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:
|
|
@@ -232,27 +235,28 @@ 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
|
)
|
|
251
255
|
# Option+B: navigate to bell (next agent needing attention)
|
|
252
256
|
_tmux(
|
|
253
257
|
"bind-key", "-n", "M-b",
|
|
254
258
|
"if-shell", "-F", _in_bottom,
|
|
255
|
-
f"send-keys -t overcode:{SPLIT_WINDOW_NAME}.
|
|
259
|
+
f"send-keys -t overcode:{SPLIT_WINDOW_NAME}.{_base} b",
|
|
256
260
|
"send-keys M-b",
|
|
257
261
|
)
|
|
258
262
|
|
|
@@ -263,12 +267,12 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
|
|
|
263
267
|
# copy mode in the inner session via the tmux API.
|
|
264
268
|
if linked_session:
|
|
265
269
|
# PageUp: enter copy mode + scroll up, but only when in the
|
|
266
|
-
# bottom pane (pane_index !=
|
|
270
|
+
# bottom pane (pane_index != base) of the split window.
|
|
267
271
|
# Top pane (Textual TUI) and other windows get normal PageUp.
|
|
268
272
|
_tmux(
|
|
269
273
|
"bind-key", "-n", "PPage",
|
|
270
274
|
"if-shell", "-F",
|
|
271
|
-
f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},
|
|
275
|
+
f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},{_base}}}}}",
|
|
272
276
|
f"copy-mode -t {linked_session} -u",
|
|
273
277
|
"send-keys PPage",
|
|
274
278
|
)
|
|
@@ -276,7 +280,7 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
|
|
|
276
280
|
_tmux(
|
|
277
281
|
"bind-key", "-n", "NPage",
|
|
278
282
|
"if-shell", "-F",
|
|
279
|
-
f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},
|
|
283
|
+
f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},#{{!=:#{{pane_index}},{_base}}}}}",
|
|
280
284
|
f"send-keys -t {linked_session} NPage",
|
|
281
285
|
"send-keys NPage",
|
|
282
286
|
)
|
|
@@ -288,7 +292,7 @@ def _setup_keybindings(linked_session: str = "", toggle_key: str = "") -> None:
|
|
|
288
292
|
# already active), then send scroll-up/down commands to it.
|
|
289
293
|
_in_bottom = (
|
|
290
294
|
f"#{{&&:#{{==:#{{window_name}},{SPLIT_WINDOW_NAME}}},"
|
|
291
|
-
f"#{{!=:#{{pane_index}},
|
|
295
|
+
f"#{{!=:#{{pane_index}},{_base}}}}}"
|
|
292
296
|
)
|
|
293
297
|
_tmux(
|
|
294
298
|
"bind-key", "-n", "WheelUpPane",
|
|
@@ -410,7 +414,7 @@ def tmux_resize():
|
|
|
410
414
|
|
|
411
415
|
# Apply the new ratio
|
|
412
416
|
new_height = max(int(int(info.split(":")[1]) * next_ratio / 100), 5)
|
|
413
|
-
_tmux("resize-pane", "-t", f"{target}.
|
|
417
|
+
_tmux("resize-pane", "-t", f"{target}.{get_pane_base_index()}", "-y", str(new_height))
|
|
414
418
|
|
|
415
419
|
|
|
416
420
|
@app.command("tmux")
|
|
@@ -601,20 +605,30 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
|
|
|
601
605
|
if existing:
|
|
602
606
|
if _is_split_window_healthy(existing):
|
|
603
607
|
if in_tmux:
|
|
604
|
-
# Check if a real (non-
|
|
608
|
+
# Check if a real (non-nested) client is already on overcode.
|
|
605
609
|
# If so, no switch needed — the user is already there or
|
|
606
610
|
# another terminal has it open. Switching blindly can
|
|
607
611
|
# accidentally move the bottom pane's nested client from
|
|
608
612
|
# oc-view-agents to overcode, creating a recursive display
|
|
609
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
|
+
)
|
|
610
624
|
oc_clients = _tmux_output(
|
|
611
625
|
"list-clients", "-t", oc_session,
|
|
612
|
-
"-F", "#{
|
|
626
|
+
"-F", "#{client_tty}",
|
|
613
627
|
)
|
|
614
628
|
has_real_client = any(
|
|
615
|
-
|
|
616
|
-
for
|
|
617
|
-
if
|
|
629
|
+
tty.strip() not in pane_ttys
|
|
630
|
+
for tty in oc_clients.splitlines()
|
|
631
|
+
if tty.strip()
|
|
618
632
|
)
|
|
619
633
|
if not has_real_client:
|
|
620
634
|
# No real client on overcode yet — switch the caller's
|
|
@@ -632,14 +646,14 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
|
|
|
632
646
|
else:
|
|
633
647
|
# Respawn before attach — execlp replaces this process
|
|
634
648
|
_tmux("respawn-pane", "-k",
|
|
635
|
-
"-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.
|
|
649
|
+
"-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}",
|
|
636
650
|
monitor_cmd)
|
|
637
651
|
rprint(f"[green]Attaching to existing {SPLIT_WINDOW_NAME} window (monitor restarted)...[/green]")
|
|
638
652
|
time.sleep(0.2)
|
|
639
653
|
os.execlp("tmux", "tmux", "attach-session", "-t", oc_session)
|
|
640
654
|
# Always restart the monitor so code changes take effect
|
|
641
655
|
_tmux("respawn-pane", "-k",
|
|
642
|
-
"-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.
|
|
656
|
+
"-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}",
|
|
643
657
|
monitor_cmd)
|
|
644
658
|
rprint(f"[green]Switched to existing {SPLIT_WINDOW_NAME} window (monitor restarted)[/green]")
|
|
645
659
|
return
|
|
@@ -668,7 +682,7 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
|
|
|
668
682
|
|
|
669
683
|
# The bottom pane runs a nested tmux attach. We must unset $TMUX
|
|
670
684
|
# because tmux refuses to attach from inside an existing session.
|
|
671
|
-
attach_cmd = f"unset TMUX; tmux attach-session -t {linked}"
|
|
685
|
+
attach_cmd = f"sh -c 'unset TMUX; exec tmux attach-session -t {linked}'"
|
|
672
686
|
|
|
673
687
|
# If an "overcode" session exists but has no split window, it might be:
|
|
674
688
|
# (a) a stale session from a previous overcode run, or
|
|
@@ -749,7 +763,7 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
|
|
|
749
763
|
result = _tmux(
|
|
750
764
|
"split-window", "-v",
|
|
751
765
|
"-t", f"{oc_session}:{SPLIT_WINDOW_NAME}",
|
|
752
|
-
"-
|
|
766
|
+
"-l", f"{100 - ratio}%",
|
|
753
767
|
attach_cmd,
|
|
754
768
|
)
|
|
755
769
|
if result.returncode != 0:
|
|
@@ -757,17 +771,23 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
|
|
|
757
771
|
raise typer.Exit(1)
|
|
758
772
|
|
|
759
773
|
# Focus top pane
|
|
760
|
-
_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()}")
|
|
761
775
|
|
|
762
776
|
rprint(f"[green]Split layout ready.[/green] Tab toggles panes.")
|
|
763
777
|
|
|
764
778
|
else:
|
|
765
779
|
# --- Outside tmux: create detached, split, then attach ---
|
|
766
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"
|
|
767
787
|
result = _tmux(
|
|
768
788
|
"new-session", "-d", "-s", oc_session,
|
|
769
789
|
"-n", SPLIT_WINDOW_NAME,
|
|
770
|
-
"-x",
|
|
790
|
+
"-x", term_x, "-y", term_y,
|
|
771
791
|
monitor_cmd,
|
|
772
792
|
)
|
|
773
793
|
if result.returncode != 0:
|
|
@@ -792,12 +812,12 @@ def _tmux_layout_locked(session: str, ratio: int, rprint) -> None:
|
|
|
792
812
|
_tmux(
|
|
793
813
|
"split-window", "-v",
|
|
794
814
|
"-t", f"{oc_session}:{SPLIT_WINDOW_NAME}",
|
|
795
|
-
"-
|
|
815
|
+
"-l", f"{100 - ratio}%",
|
|
796
816
|
attach_cmd,
|
|
797
817
|
)
|
|
798
818
|
|
|
799
819
|
# Focus top pane
|
|
800
|
-
_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()}")
|
|
801
821
|
|
|
802
822
|
# Attach to the session (replaces this process)
|
|
803
823
|
rprint(f"[green]Attaching to split layout...[/green]")
|
|
@@ -111,23 +111,38 @@ class JobLauncher:
|
|
|
111
111
|
if not detect_killed:
|
|
112
112
|
return jobs
|
|
113
113
|
|
|
114
|
-
# Get list of actual tmux windows
|
|
114
|
+
# Get list of actual tmux windows.
|
|
115
|
+
# If the list is empty, it's likely a query failure (stale libtmux
|
|
116
|
+
# session cache or transient error) rather than all windows being
|
|
117
|
+
# gone — skip kill detection entirely to avoid false kills (#396).
|
|
115
118
|
windows = self.tmux.list_windows()
|
|
119
|
+
if not windows:
|
|
120
|
+
return jobs
|
|
116
121
|
window_names = {w['name'] for w in windows}
|
|
117
122
|
|
|
123
|
+
now = datetime.now()
|
|
118
124
|
for job in jobs:
|
|
119
125
|
if job.status != "running":
|
|
120
126
|
continue
|
|
121
127
|
|
|
122
128
|
if job.tmux_window not in window_names:
|
|
129
|
+
# Grace period: don't mark as killed until the job has been
|
|
130
|
+
# "missing" for at least 30s, to avoid race conditions during
|
|
131
|
+
# window creation (#396).
|
|
132
|
+
try:
|
|
133
|
+
age = (now - datetime.fromisoformat(job.start_time)).total_seconds()
|
|
134
|
+
except (ValueError, TypeError):
|
|
135
|
+
age = 0
|
|
136
|
+
if age < 30:
|
|
137
|
+
continue
|
|
123
138
|
# Window gone but no _complete called → killed
|
|
124
139
|
self.jobs.update_job(
|
|
125
140
|
job.id,
|
|
126
141
|
status="killed",
|
|
127
|
-
end_time=
|
|
142
|
+
end_time=now.isoformat(),
|
|
128
143
|
)
|
|
129
144
|
job.status = "killed"
|
|
130
|
-
job.end_time =
|
|
145
|
+
job.end_time = now.isoformat()
|
|
131
146
|
continue
|
|
132
147
|
|
|
133
148
|
# Window still alive — check if the command has finished by
|
|
@@ -508,7 +508,7 @@ class TUIPreferences:
|
|
|
508
508
|
# Only stores explicit user overrides. Missing = use default from detail_levels.
|
|
509
509
|
column_config: dict = field(default_factory=dict)
|
|
510
510
|
# Show abbreviated column headers above summary lines
|
|
511
|
-
show_column_headers: bool =
|
|
511
|
+
show_column_headers: bool = True
|
|
512
512
|
# Sister instances hidden from agent list (#323)
|
|
513
513
|
disabled_sisters: Set[str] = field(default_factory=set)
|
|
514
514
|
# Log every status change to diagnostics CSV (off by default)
|
|
@@ -170,11 +170,18 @@ class PollingStatusDetector:
|
|
|
170
170
|
# But if a user prompt is visible, the agent is waiting — content
|
|
171
171
|
# changes from TUI refreshes or status-bar updates shouldn't override
|
|
172
172
|
# prompt detection.
|
|
173
|
+
#
|
|
174
|
+
# HOWEVER: Claude Code always renders the ❯ prompt as UI chrome, even
|
|
175
|
+
# while actively working (#393). When active indicators like
|
|
176
|
+
# "esc to interrupt" are present, the prompt is just decoration —
|
|
177
|
+
# trust the active indicators over the prompt char.
|
|
173
178
|
if content_changed:
|
|
174
|
-
|
|
175
|
-
if
|
|
176
|
-
self.
|
|
177
|
-
|
|
179
|
+
has_active_indicator = matches_any(last_few, self.patterns.active_indicators)
|
|
180
|
+
if not has_active_indicator:
|
|
181
|
+
prompt_result = self._detect_user_prompt(last_lines, content)
|
|
182
|
+
if prompt_result is not None:
|
|
183
|
+
self._last_detect_phase[session.id] = "P5+P12:prompt_override"
|
|
184
|
+
return prompt_result
|
|
178
185
|
activity = self._extract_last_activity(last_lines)
|
|
179
186
|
self._last_detect_phase[session.id] = "P5:content_changed"
|
|
180
187
|
return STATUS_RUNNING, f"Active: {activity}", content
|
|
@@ -33,18 +33,23 @@ fi
|
|
|
33
33
|
# Create new session with the TUI
|
|
34
34
|
tmux new-session -d -s "$CONTROLLER_SESSION" -n "controller"
|
|
35
35
|
|
|
36
|
+
# Query pane-base-index (default 0, commonly set to 1)
|
|
37
|
+
PANE_BASE=$(tmux show-options -gv pane-base-index 2>/dev/null || echo 0)
|
|
38
|
+
PANE_BASE=${PANE_BASE:-0}
|
|
39
|
+
PANE_BOTTOM=$((PANE_BASE + 1))
|
|
40
|
+
|
|
36
41
|
# Split window horizontally (top 33%, bottom 66%)
|
|
37
|
-
tmux split-window -v -
|
|
42
|
+
tmux split-window -v -l 66% -t "$CONTROLLER_SESSION:0"
|
|
38
43
|
|
|
39
44
|
# Top pane: Run the TUI (without piping to preserve terminal control)
|
|
40
|
-
tmux send-keys -t "$CONTROLLER_SESSION:0
|
|
45
|
+
tmux send-keys -t "$CONTROLLER_SESSION:0.$PANE_BASE" "PYTHONUNBUFFERED=1 python -m overcode.tui $SESSION_NAME" C-m
|
|
41
46
|
|
|
42
47
|
# Bottom pane: Launch Claude (no auto-prompt - let user interact naturally)
|
|
43
|
-
tmux send-keys -t "$CONTROLLER_SESSION:0
|
|
48
|
+
tmux send-keys -t "$CONTROLLER_SESSION:0.$PANE_BOTTOM" "claude" C-m
|
|
44
49
|
|
|
45
50
|
# Set pane titles
|
|
46
|
-
tmux select-pane -t "$CONTROLLER_SESSION:0
|
|
47
|
-
tmux select-pane -t "$CONTROLLER_SESSION:0
|
|
51
|
+
tmux select-pane -t "$CONTROLLER_SESSION:0.$PANE_BASE" -T "Overcode Monitor"
|
|
52
|
+
tmux select-pane -t "$CONTROLLER_SESSION:0.$PANE_BOTTOM" -T "Controller"
|
|
48
53
|
|
|
49
54
|
# Attach to the session
|
|
50
55
|
exec tmux attach-session -t "$CONTROLLER_SESSION"
|
|
@@ -109,6 +109,28 @@ def attach_bare(session_name: str, window_name: str, socket_path: str = None) ->
|
|
|
109
109
|
os.execlp(tmux_cmd[0], *tmux_cmd, "attach-session", "-t", bare_session)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
_pane_base_index: Optional[int] = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_pane_base_index() -> int:
|
|
116
|
+
"""Return the tmux pane-base-index setting (default 0, commonly set to 1).
|
|
117
|
+
|
|
118
|
+
The result is cached for the lifetime of the process.
|
|
119
|
+
"""
|
|
120
|
+
global _pane_base_index
|
|
121
|
+
if _pane_base_index is not None:
|
|
122
|
+
return _pane_base_index
|
|
123
|
+
try:
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
_build_tmux_cmd() + ["show-options", "-gv", "pane-base-index"],
|
|
126
|
+
capture_output=True, text=True, timeout=5,
|
|
127
|
+
)
|
|
128
|
+
_pane_base_index = int(result.stdout.strip()) if result.returncode == 0 and result.stdout.strip() else 0
|
|
129
|
+
except (subprocess.TimeoutExpired, ValueError, OSError):
|
|
130
|
+
_pane_base_index = 0
|
|
131
|
+
return _pane_base_index
|
|
132
|
+
|
|
133
|
+
|
|
112
134
|
def tmux_window_target(session: str, window) -> str:
|
|
113
135
|
"""Build tmux target string for a window.
|
|
114
136
|
|
|
@@ -44,6 +44,7 @@ from .summarizer_component import (
|
|
|
44
44
|
from .sister_poller import SisterPoller
|
|
45
45
|
from .usage_monitor import UsageMonitor
|
|
46
46
|
from .implementations import RealTmux
|
|
47
|
+
from .tmux_utils import get_pane_base_index
|
|
47
48
|
from .tui_helpers import (
|
|
48
49
|
format_duration,
|
|
49
50
|
get_git_diff_stats,
|
|
@@ -128,8 +129,8 @@ class SupervisorTUI(
|
|
|
128
129
|
("backslash", "monitor_restart", "Restart monitor"),
|
|
129
130
|
("a", "focus_human_annotation", "Annotation"),
|
|
130
131
|
("A", "toggle_summarizer", "AI summarizer"),
|
|
131
|
-
#
|
|
132
|
-
("r", "resize_focused_window", "Resize
|
|
132
|
+
# Resize focused agent's tmux window to match pane size
|
|
133
|
+
("r", "resize_focused_window", "Resize pane"),
|
|
133
134
|
# Agent management
|
|
134
135
|
("x", "kill_focused", "Kill/Clean up"),
|
|
135
136
|
("R", "restart_focused", "Restart agent"),
|
|
@@ -868,6 +869,16 @@ class SupervisorTUI(
|
|
|
868
869
|
pass
|
|
869
870
|
return False
|
|
870
871
|
|
|
872
|
+
def _tui_pane_target(self) -> str:
|
|
873
|
+
"""Return tmux target for the TUI (top) pane, respecting pane-base-index."""
|
|
874
|
+
base = get_pane_base_index()
|
|
875
|
+
return f"overcode:overcode-tmux.{base}"
|
|
876
|
+
|
|
877
|
+
def _bottom_pane_target(self) -> str:
|
|
878
|
+
"""Return tmux target for the bottom (terminal) pane, respecting pane-base-index."""
|
|
879
|
+
base = get_pane_base_index()
|
|
880
|
+
return f"overcode:overcode-tmux.{base + 1}"
|
|
881
|
+
|
|
871
882
|
def _dialog_will_open(self) -> None:
|
|
872
883
|
"""Zoom the tmux monitor pane when a dialog opens (compact mode only).
|
|
873
884
|
|
|
@@ -877,7 +888,7 @@ class SupervisorTUI(
|
|
|
877
888
|
if not self.compact:
|
|
878
889
|
return
|
|
879
890
|
import subprocess
|
|
880
|
-
target =
|
|
891
|
+
target = self._tui_pane_target()
|
|
881
892
|
# Don't zoom if already zoomed
|
|
882
893
|
info = subprocess.run(
|
|
883
894
|
["tmux", "display-message", "-t", target, "-p", "#{window_zoomed_flag}"],
|
|
@@ -903,7 +914,7 @@ class SupervisorTUI(
|
|
|
903
914
|
if self.tui_mode == "jobs":
|
|
904
915
|
return
|
|
905
916
|
import subprocess
|
|
906
|
-
target =
|
|
917
|
+
target = self._tui_pane_target()
|
|
907
918
|
info = subprocess.run(
|
|
908
919
|
["tmux", "display-message", "-t", target, "-p", "#{window_zoomed_flag}"],
|
|
909
920
|
capture_output=True, text=True,
|
|
@@ -943,7 +954,7 @@ class SupervisorTUI(
|
|
|
943
954
|
# Unzoom — but only if no dialog is holding the zoom open
|
|
944
955
|
if not self._any_dialog_visible():
|
|
945
956
|
import subprocess
|
|
946
|
-
target =
|
|
957
|
+
target = self._tui_pane_target()
|
|
947
958
|
info = subprocess.run(
|
|
948
959
|
["tmux", "display-message", "-t", target, "-p", "#{window_zoomed_flag}"],
|
|
949
960
|
capture_output=True, text=True,
|
|
@@ -3170,7 +3181,7 @@ class SupervisorTUI(
|
|
|
3170
3181
|
return
|
|
3171
3182
|
import subprocess
|
|
3172
3183
|
subprocess.run(
|
|
3173
|
-
["tmux", "resize-pane", "-t",
|
|
3184
|
+
["tmux", "resize-pane", "-t", self._tui_pane_target(),
|
|
3174
3185
|
"-U" if delta > 0 else "-D", str(abs(delta))],
|
|
3175
3186
|
capture_output=True,
|
|
3176
3187
|
)
|
|
@@ -512,6 +512,10 @@ class SessionActionsMixin:
|
|
|
512
512
|
session.id,
|
|
513
513
|
current_task="Synced to main"
|
|
514
514
|
)
|
|
515
|
+
# Clear PR number — agent is back on main, old PR is stale (#391)
|
|
516
|
+
self.session_manager.update_session(
|
|
517
|
+
session.id, pr_number=None, pr_branch=None
|
|
518
|
+
)
|
|
515
519
|
else:
|
|
516
520
|
self.notify(f"Failed to send /clear to '{session_name}'", severity="error")
|
|
517
521
|
|
|
@@ -84,7 +84,7 @@ class ViewActionsMixin:
|
|
|
84
84
|
|
|
85
85
|
# Get the bottom pane's actual dimensions (pane 1 in the split)
|
|
86
86
|
result = subprocess.run(
|
|
87
|
-
["tmux", "display-message", "-t",
|
|
87
|
+
["tmux", "display-message", "-t", self._bottom_pane_target(),
|
|
88
88
|
"-p", "#{pane_width} #{pane_height}"],
|
|
89
89
|
capture_output=True, text=True,
|
|
90
90
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|