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.
Files changed (119) hide show
  1. {overcode-0.3.1/src/overcode.egg-info → overcode-0.3.2}/PKG-INFO +1 -1
  2. {overcode-0.3.1 → overcode-0.3.2}/pyproject.toml +1 -1
  3. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/split.py +44 -24
  4. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/job_launcher.py +18 -3
  5. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/settings.py +1 -1
  6. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_detector.py +11 -4
  7. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/supervisor_layout.sh +10 -5
  8. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tmux_manager.py +1 -0
  9. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tmux_utils.py +22 -0
  10. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui.py +17 -6
  11. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/session.py +4 -0
  12. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/view.py +1 -1
  13. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/help_overlay.py +1 -1
  14. {overcode-0.3.1 → overcode-0.3.2/src/overcode.egg-info}/PKG-INFO +1 -1
  15. {overcode-0.3.1 → overcode-0.3.2}/LICENSE +0 -0
  16. {overcode-0.3.1 → overcode-0.3.2}/MANIFEST.in +0 -0
  17. {overcode-0.3.1 → overcode-0.3.2}/README.md +0 -0
  18. {overcode-0.3.1 → overcode-0.3.2}/setup.cfg +0 -0
  19. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/__init__.py +0 -0
  20. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/agent_scanner.py +0 -0
  21. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/bundled_skills.py +0 -0
  22. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/claude_config.py +0 -0
  23. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/claude_pid.py +0 -0
  24. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/__init__.py +0 -0
  25. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/__main__.py +0 -0
  26. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/_shared.py +0 -0
  27. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/agent.py +0 -0
  28. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/budget.py +0 -0
  29. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/config.py +0 -0
  30. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/daemon.py +0 -0
  31. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/hooks.py +0 -0
  32. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/jobs.py +0 -0
  33. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/monitoring.py +0 -0
  34. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/perms.py +0 -0
  35. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/sister.py +0 -0
  36. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/cli/skills.py +0 -0
  37. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/config.py +0 -0
  38. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/daemon_claude_skill.md +0 -0
  39. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/daemon_logging.py +0 -0
  40. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/daemon_utils.py +0 -0
  41. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/data_export.py +0 -0
  42. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/dependency_check.py +0 -0
  43. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/duration.py +0 -0
  44. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/exceptions.py +0 -0
  45. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/follow_mode.py +0 -0
  46. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/history_reader.py +0 -0
  47. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/hook_handler.py +0 -0
  48. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/hook_status_detector.py +0 -0
  49. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/implementations.py +0 -0
  50. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/interfaces.py +0 -0
  51. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/job_manager.py +0 -0
  52. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/launcher.py +0 -0
  53. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/logging_config.py +0 -0
  54. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/mocks.py +0 -0
  55. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/monitor_daemon.py +0 -0
  56. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/monitor_daemon_core.py +0 -0
  57. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/monitor_daemon_state.py +0 -0
  58. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/notifier.py +0 -0
  59. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/pid_utils.py +0 -0
  60. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/presence_logger.py +0 -0
  61. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/protocols.py +0 -0
  62. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/session_manager.py +0 -0
  63. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/sister_controller.py +0 -0
  64. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/sister_poller.py +0 -0
  65. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/standing_instructions.py +0 -0
  66. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_constants.py +0 -0
  67. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_detector_factory.py +0 -0
  68. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_history.py +0 -0
  69. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/status_patterns.py +0 -0
  70. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summarizer_client.py +0 -0
  71. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summarizer_component.py +0 -0
  72. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summary_columns.py +0 -0
  73. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/summary_groups.py +0 -0
  74. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/supervisor_daemon.py +0 -0
  75. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/supervisor_daemon_core.py +0 -0
  76. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/__init__.py +0 -0
  77. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/renderer.py +0 -0
  78. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/tmux_driver.py +0 -0
  79. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/tui_eye.py +0 -0
  80. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/testing/tui_eye_skill.md +0 -0
  81. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/time_context.py +0 -0
  82. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui.tcss +0 -0
  83. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/__init__.py +0 -0
  84. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/daemon.py +0 -0
  85. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/input.py +0 -0
  86. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_actions/navigation.py +0 -0
  87. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_helpers.py +0 -0
  88. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_logic.py +0 -0
  89. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_render.py +0 -0
  90. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/__init__.py +0 -0
  91. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  92. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/command_bar.py +0 -0
  93. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  94. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  95. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  96. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  97. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/job_summary.py +0 -0
  98. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  99. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/preview_pane.py +0 -0
  100. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/session_summary.py +0 -0
  101. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  102. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/status_timeline.py +0 -0
  103. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  104. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/usage_monitor.py +0 -0
  105. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web/__init__.py +0 -0
  106. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web/templates/analytics.html +0 -0
  107. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web/templates/dashboard.html +0 -0
  108. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_api.py +0 -0
  109. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_chartjs.py +0 -0
  110. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_control_api.py +0 -0
  111. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_server.py +0 -0
  112. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_server_runner.py +0 -0
  113. {overcode-0.3.1 → overcode-0.3.2}/src/overcode/web_templates.py +0 -0
  114. {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/SOURCES.txt +0 -0
  115. {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/dependency_links.txt +0 -0
  116. {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/entry_points.txt +0 -0
  117. {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/requires.txt +0 -0
  118. {overcode-0.3.1 → overcode-0.3.2}/src/overcode.egg-info/top_level.txt +0 -0
  119. {overcode-0.3.1 → 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.1
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.1"
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"}
@@ -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:
@@ -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}},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
  )
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}.0 b",
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 != 0) of the split window.
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}},0}}}}",
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}},0}}}}",
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}},0}}}}"
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}.0", "-y", str(new_height))
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-tiny) client is already on overcode.
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", "#{client_height}",
626
+ "-F", "#{client_tty}",
613
627
  )
614
628
  has_real_client = any(
615
- int(h) > 10
616
- for h in oc_clients.splitlines()
617
- if h.strip().isdigit()
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}.0",
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}.0",
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
- "-p", str(100 - ratio),
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}.0")
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", "200", "-y", "50",
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
- "-p", str(100 - ratio),
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}.0")
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=datetime.now().isoformat(),
142
+ end_time=now.isoformat(),
128
143
  )
129
144
  job.status = "killed"
130
- job.end_time = datetime.now().isoformat()
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 = False
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
- prompt_result = self._detect_user_prompt(last_lines, content)
175
- if prompt_result is not None:
176
- self._last_detect_phase[session.id] = "P5+P12:prompt_override"
177
- return prompt_result
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 -p 66 -t "$CONTROLLER_SESSION:0"
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.0" "PYTHONUNBUFFERED=1 python -m overcode.tui $SESSION_NAME" C-m
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.1" "claude" C-m
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.0" -T "Overcode Monitor"
47
- tmux select-pane -t "$CONTROLLER_SESSION:0.1" -T "Controller"
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"
@@ -53,6 +53,7 @@ class TmuxManager:
53
53
  except (LibTmuxException, ObjectDoesNotExist):
54
54
  return None
55
55
 
56
+
56
57
  def _get_window(self, window_name: str) -> Optional[libtmux.Window]:
57
58
  """Get a window by name.
58
59
 
@@ -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
- # Manual refresh (useful in diagnostics mode)
132
- ("r", "resize_focused_window", "Resize window"),
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 = "overcode:overcode-tmux.0"
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 = "overcode:overcode-tmux.0"
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 = "overcode:overcode-tmux.0"
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", "overcode:overcode-tmux.0",
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", "overcode:overcode-tmux.1",
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
  )
@@ -39,7 +39,7 @@ class HelpOverlay(Static):
39
39
 
40
40
  section("VIEW")
41
41
  row("m", "Toggle preview pane", "f", "Fullscreen preview")
42
- row("r", "Refresh")
42
+ row("r", "Resize pane")
43
43
  t.append("\n")
44
44
 
45
45
  section("DISPLAY")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overcode
3
- Version: 0.3.1
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes