overcode 0.2.5__tar.gz → 0.2.6__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 (111) hide show
  1. {overcode-0.2.5/src/overcode.egg-info → overcode-0.2.6}/PKG-INFO +1 -1
  2. {overcode-0.2.5 → overcode-0.2.6}/pyproject.toml +1 -1
  3. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/agent.py +82 -6
  4. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/daemon_utils.py +17 -0
  5. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/follow_mode.py +9 -8
  6. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/hook_status_detector.py +4 -4
  7. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/implementations.py +30 -15
  8. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/launcher.py +197 -22
  9. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/mocks.py +12 -14
  10. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/monitor_daemon.py +27 -24
  11. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/monitor_daemon_state.py +1 -1
  12. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/pid_utils.py +63 -1
  13. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/protocols.py +13 -13
  14. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/session_manager.py +11 -5
  15. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/sister_poller.py +1 -1
  16. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/status_constants.py +1 -0
  17. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/status_detector.py +35 -27
  18. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/status_detector_factory.py +3 -3
  19. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/status_patterns.py +28 -14
  20. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/summarizer_component.py +2 -2
  21. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/supervisor_daemon.py +16 -16
  22. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tmux_manager.py +49 -22
  23. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tmux_utils.py +18 -6
  24. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui.py +90 -73
  25. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_actions/daemon.py +16 -22
  26. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_actions/session.py +41 -0
  27. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_logic.py +25 -0
  28. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/command_bar.py +22 -0
  29. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/help_overlay.py +1 -1
  30. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/preview_pane.py +20 -1
  31. {overcode-0.2.5 → overcode-0.2.6/src/overcode.egg-info}/PKG-INFO +1 -1
  32. {overcode-0.2.5 → overcode-0.2.6}/LICENSE +0 -0
  33. {overcode-0.2.5 → overcode-0.2.6}/MANIFEST.in +0 -0
  34. {overcode-0.2.5 → overcode-0.2.6}/README.md +0 -0
  35. {overcode-0.2.5 → overcode-0.2.6}/setup.cfg +0 -0
  36. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/__init__.py +0 -0
  37. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/agent_scanner.py +0 -0
  38. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/bundled_skills.py +0 -0
  39. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/claude_config.py +0 -0
  40. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/__init__.py +0 -0
  41. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/__main__.py +0 -0
  42. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/_shared.py +0 -0
  43. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/budget.py +0 -0
  44. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/config.py +0 -0
  45. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/daemon.py +0 -0
  46. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/hooks.py +0 -0
  47. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/monitoring.py +0 -0
  48. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/perms.py +0 -0
  49. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/sister.py +0 -0
  50. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/cli/skills.py +0 -0
  51. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/config.py +0 -0
  52. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/daemon_claude_skill.md +0 -0
  53. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/daemon_logging.py +0 -0
  54. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/data_export.py +0 -0
  55. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/dependency_check.py +0 -0
  56. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/exceptions.py +0 -0
  57. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/history_reader.py +0 -0
  58. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/hook_handler.py +0 -0
  59. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/interfaces.py +0 -0
  60. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/logging_config.py +0 -0
  61. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/monitor_daemon_core.py +0 -0
  62. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/notifier.py +0 -0
  63. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/presence_logger.py +0 -0
  64. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/settings.py +0 -0
  65. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/sister_controller.py +0 -0
  66. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/standing_instructions.py +0 -0
  67. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/status_history.py +0 -0
  68. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/summarizer_client.py +0 -0
  69. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/summary_columns.py +0 -0
  70. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/summary_groups.py +0 -0
  71. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/supervisor_daemon_core.py +0 -0
  72. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/supervisor_layout.sh +0 -0
  73. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/testing/__init__.py +0 -0
  74. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/testing/renderer.py +0 -0
  75. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/testing/tmux_driver.py +0 -0
  76. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/testing/tui_eye.py +0 -0
  77. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/testing/tui_eye_skill.md +0 -0
  78. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/time_context.py +0 -0
  79. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui.tcss +0 -0
  80. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_actions/__init__.py +0 -0
  81. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_actions/input.py +0 -0
  82. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_actions/navigation.py +0 -0
  83. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_actions/view.py +0 -0
  84. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_helpers.py +0 -0
  85. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_render.py +0 -0
  86. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/__init__.py +0 -0
  87. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  88. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  89. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/daemon_status_bar.py +0 -0
  90. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  91. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  92. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/session_summary.py +0 -0
  93. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  94. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/status_timeline.py +0 -0
  95. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  96. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/usage_monitor.py +0 -0
  97. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web/__init__.py +0 -0
  98. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web/templates/analytics.html +0 -0
  99. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web/templates/dashboard.html +0 -0
  100. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web_api.py +0 -0
  101. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web_chartjs.py +0 -0
  102. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web_control_api.py +0 -0
  103. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web_server.py +0 -0
  104. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web_server_runner.py +0 -0
  105. {overcode-0.2.5 → overcode-0.2.6}/src/overcode/web_templates.py +0 -0
  106. {overcode-0.2.5 → overcode-0.2.6}/src/overcode.egg-info/SOURCES.txt +0 -0
  107. {overcode-0.2.5 → overcode-0.2.6}/src/overcode.egg-info/dependency_links.txt +0 -0
  108. {overcode-0.2.5 → overcode-0.2.6}/src/overcode.egg-info/entry_points.txt +0 -0
  109. {overcode-0.2.5 → overcode-0.2.6}/src/overcode.egg-info/requires.txt +0 -0
  110. {overcode-0.2.5 → overcode-0.2.6}/src/overcode.egg-info/top_level.txt +0 -0
  111. {overcode-0.2.5 → overcode-0.2.6}/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.2.5
3
+ Version: 0.2.6
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.2.5"
7
+ version = "0.2.6"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -212,6 +212,68 @@ def launch(
212
212
  rprint("\nTo view: [bold]overcode attach[/bold]")
213
213
 
214
214
 
215
+ @app.command()
216
+ def fork(
217
+ source: Annotated[str, typer.Argument(help="Name of agent to fork from")],
218
+ name: Annotated[
219
+ Optional[str], typer.Option("--name", "-n", help="Name for the forked agent (default: <source>-fork)")
220
+ ] = None,
221
+ prompt: Annotated[
222
+ Optional[str], typer.Option("--prompt", "-p", help="Initial prompt to send to the fork")
223
+ ] = None,
224
+ session: SessionOption = "agents",
225
+ ):
226
+ """Fork an agent, creating a child with the source's conversation context.
227
+
228
+ Uses Claude's --resume --fork-session to branch the conversation history
229
+ into a new independent agent. The forked agent inherits the source's
230
+ directory, permissions, agent persona, and CLI flags.
231
+
232
+ Examples:
233
+ overcode fork my-agent # Fork as my-agent-fork
234
+ overcode fork my-agent -n side-analysis # Fork with custom name
235
+ overcode fork my-agent -p "analyze test failures" # Fork with initial prompt
236
+ """
237
+ from ..session_manager import SessionManager
238
+
239
+ sm = SessionManager()
240
+ source_session = sm.get_session_by_name(source)
241
+ if not source_session:
242
+ rprint(f"[red]Error: Agent '{source}' not found[/red]")
243
+ raise typer.Exit(code=1)
244
+
245
+ if not source_session.active_claude_session_id:
246
+ rprint(f"[red]Error: Agent '{source}' has no active Claude session ID yet[/red]")
247
+ rprint("[dim]The monitor daemon detects session IDs periodically — try again shortly[/dim]")
248
+ raise typer.Exit(code=1)
249
+
250
+ if source_session.status in ("terminated", "done"):
251
+ rprint(f"[red]Error: Cannot fork a {source_session.status} agent[/red]")
252
+ raise typer.Exit(code=1)
253
+
254
+ # Default fork name
255
+ fork_name = name if name else f"{source}-fork"
256
+
257
+ launcher = ClaudeLauncher(session)
258
+ result = launcher.launch_fork(
259
+ name=fork_name,
260
+ source_session=source_session,
261
+ initial_prompt=prompt,
262
+ )
263
+
264
+ if result:
265
+ rprint(f"\n[green]✓[/green] Forked '[bold]{source}[/bold]' → '[bold]{fork_name}[/bold]'")
266
+ rprint(f" Directory: {result.start_directory}")
267
+ if result.claude_agent:
268
+ rprint(f" Agent: {result.claude_agent}")
269
+ if prompt:
270
+ rprint(" Initial prompt sent")
271
+ rprint(f"\nTo view: [bold]overcode attach {fork_name}[/bold]")
272
+ else:
273
+ rprint(f"[red]Error: Failed to fork '{source}'[/red]")
274
+ raise typer.Exit(code=1)
275
+
276
+
215
277
  @app.command("list")
216
278
  def list_agents(
217
279
  name: Annotated[
@@ -788,6 +850,9 @@ def show(
788
850
  stats_only: Annotated[
789
851
  bool, typer.Option("--stats-only", "-s", help="Show only stats, no pane output")
790
852
  ] = False,
853
+ color: Annotated[
854
+ bool, typer.Option("--color", "-c", help="Preserve ANSI colors in pane output")
855
+ ] = False,
791
856
  session: SessionOption = "agents",
792
857
  ):
793
858
  """Show agent details and recent output."""
@@ -908,12 +973,23 @@ def show(
908
973
  # Pane output section (skip if --stats-only or --lines 0)
909
974
  if not stats_only and lines > 0:
910
975
  if pane_content_raw:
911
- clean_content = strip_ansi(pane_content_raw)
912
- content_lines = clean_content.rstrip().split('\n')
913
- display_lines = content_lines[-lines:]
914
- print(f"=== {name} (last {lines} lines) ===")
915
- print('\n'.join(display_lines))
916
- print(f"=== end {name} ===")
976
+ if color:
977
+ from rich.console import Console
978
+ from rich.text import Text
979
+ console = Console()
980
+ raw_lines = pane_content_raw.rstrip().split('\n')
981
+ display_lines = raw_lines[-lines:]
982
+ console.print(f"=== {name} (last {lines} lines) ===")
983
+ for line in display_lines:
984
+ console.print(Text.from_ansi(line))
985
+ console.print(f"=== end {name} ===")
986
+ else:
987
+ clean_content = strip_ansi(pane_content_raw)
988
+ content_lines = clean_content.rstrip().split('\n')
989
+ display_lines = content_lines[-lines:]
990
+ print(f"=== {name} (last {lines} lines) ===")
991
+ print('\n'.join(display_lines))
992
+ print(f"=== end {name} ===")
917
993
  else:
918
994
  # Fallback for terminated sessions
919
995
  output = launcher.get_session_output(name, lines=lines)
@@ -78,12 +78,29 @@ def create_daemon_helpers(
78
78
  os.kill(pid, signal.SIGTERM)
79
79
  # Wait for process to actually terminate before removing PID file
80
80
  start = time.time()
81
+ terminated = False
81
82
  while time.time() - start < 5.0:
82
83
  try:
83
84
  os.kill(pid, 0)
84
85
  time.sleep(0.1)
85
86
  except (OSError, ProcessLookupError):
87
+ terminated = True
86
88
  break
89
+
90
+ if not terminated:
91
+ # Still alive after timeout — force kill
92
+ try:
93
+ os.kill(pid, signal.SIGKILL)
94
+ time.sleep(0.1)
95
+ except (OSError, ProcessLookupError):
96
+ pass
97
+
98
+ # Reap zombie if we're the parent process
99
+ try:
100
+ os.waitpid(pid, os.WNOHANG)
101
+ except ChildProcessError:
102
+ pass # Not our child, or already reaped
103
+
87
104
  remove_pid_file(pid_path)
88
105
  return True
89
106
  except (OSError, ProcessLookupError):
@@ -19,15 +19,16 @@ from .session_manager import SessionManager
19
19
  from .status_patterns import strip_ansi
20
20
  from .settings import get_session_dir
21
21
  from .status_constants import DEFAULT_CAPTURE_LINES, STATUS_WAITING_OVERSIGHT
22
+ from .tmux_utils import tmux_window_target
22
23
 
23
24
 
24
- def _capture_pane(tmux_session: str, window_index: int, lines: int = DEFAULT_CAPTURE_LINES) -> Optional[str]:
25
+ def _capture_pane(tmux_session: str, window_name: str, lines: int = DEFAULT_CAPTURE_LINES) -> Optional[str]:
25
26
  """Capture recent pane output via tmux."""
26
27
  try:
27
28
  result = subprocess.run(
28
29
  [
29
30
  "tmux", "capture-pane",
30
- "-t", f"{tmux_session}:{window_index}",
31
+ "-t", tmux_window_target(tmux_session, window_name),
31
32
  "-p",
32
33
  "-S", f"-{lines}",
33
34
  ],
@@ -143,7 +144,7 @@ def follow_agent(
143
144
  print(f"Error: Agent '{name}' not found", file=sys.stderr)
144
145
  return 1
145
146
 
146
- window_index = session.tmux_window
147
+ window_name = session.tmux_window
147
148
 
148
149
  # Read oversight policy from session
149
150
  oversight_policy = getattr(session, 'oversight_policy', 'wait') or 'wait'
@@ -162,7 +163,7 @@ def follow_agent(
162
163
  try:
163
164
  while not interrupted:
164
165
  # Capture pane content
165
- raw = _capture_pane(tmux_session, window_index)
166
+ raw = _capture_pane(tmux_session, window_name)
166
167
  if raw is None:
167
168
  if _check_session_terminated(sessions, name):
168
169
  print(f"\n[follow] Agent '{name}' terminated", file=sys.stderr)
@@ -176,7 +177,7 @@ def follow_agent(
176
177
  if _check_hook_stop(tmux_session, name):
177
178
  # Wait one extra cycle to capture final output
178
179
  time.sleep(poll_interval)
179
- raw = _capture_pane(tmux_session, window_index)
180
+ raw = _capture_pane(tmux_session, window_name)
180
181
  if raw:
181
182
  for line in raw.rstrip().split('\n'):
182
183
  cleaned = strip_ansi(line).strip()
@@ -226,7 +227,7 @@ def follow_agent(
226
227
 
227
228
  # Enter report-polling sub-loop
228
229
  return _poll_for_report(
229
- name, tmux_session, sessions, window_index,
230
+ name, tmux_session, sessions, window_name,
230
231
  oversight_policy, oversight_timeout_seconds,
231
232
  poll_interval, recent_lines,
232
233
  )
@@ -253,7 +254,7 @@ def _poll_for_report(
253
254
  name: str,
254
255
  tmux_session: str,
255
256
  sessions: SessionManager,
256
- window_index: int,
257
+ window_name: str,
257
258
  oversight_policy: str,
258
259
  oversight_timeout_seconds: float,
259
260
  poll_interval: float,
@@ -307,7 +308,7 @@ def _poll_for_report(
307
308
  return 1
308
309
 
309
310
  # Continue streaming pane output while waiting
310
- raw = _capture_pane(tmux_session, window_index)
311
+ raw = _capture_pane(tmux_session, window_name)
311
312
  if raw:
312
313
  _emit_new_lines(raw, recent_lines)
313
314
 
@@ -140,13 +140,13 @@ class HookStatusDetector:
140
140
 
141
141
  return data
142
142
 
143
- def get_pane_content(self, window: int, num_lines: int = 0) -> Optional[str]:
143
+ def get_pane_content(self, window: str, num_lines: int = 0) -> Optional[str]:
144
144
  """Get pane content via the polling detector's tmux interface."""
145
145
  fallback = self._get_polling_fallback()
146
146
  fallback.capture_lines = self.capture_lines
147
147
  return fallback.get_pane_content(window, num_lines)
148
148
 
149
- def detect_status(self, session: "Session") -> Tuple[str, str, str]:
149
+ def detect_status(self, session: "Session", num_lines: int = 0) -> Tuple[str, str, str]:
150
150
  """Detect session status using hook state files.
151
151
 
152
152
  When fresh hook state exists, uses that for status determination.
@@ -160,7 +160,7 @@ class HookStatusDetector:
160
160
 
161
161
  if hook_state is None:
162
162
  # No hook state or stale → full polling fallback
163
- return self._get_polling_fallback().detect_status(session)
163
+ return self._get_polling_fallback().detect_status(session, num_lines=num_lines)
164
164
 
165
165
  # Hook state is fresh → use it for status
166
166
  event = hook_state.get("event", "")
@@ -183,7 +183,7 @@ class HookStatusDetector:
183
183
  status = STATUS_WAITING_OVERSIGHT
184
184
 
185
185
  # Read pane for activity enrichment and content return value
186
- pane_content = self.get_pane_content(session.tmux_window) or ""
186
+ pane_content = self.get_pane_content(session.tmux_window, num_lines=num_lines) or ""
187
187
 
188
188
  # Check for busy-sleeping: agent is "running" but executing a sleep command (#289)
189
189
  if status == STATUS_RUNNING:
@@ -65,17 +65,27 @@ class RealTmux:
65
65
  except (LibTmuxException, ObjectDoesNotExist):
66
66
  return None
67
67
 
68
- def _get_window(self, session: str, window: int) -> Optional[libtmux.Window]:
69
- """Get a window by session name and window index."""
68
+ def _get_window(self, session: str, window: str) -> Optional[libtmux.Window]:
69
+ """Get a window by session name and window name.
70
+
71
+ Falls back to index-based lookup for legacy sessions that still have
72
+ digit-string window values (e.g. "4" from pre-name-based era).
73
+ """
70
74
  sess = self._get_session(session)
71
75
  if sess is None:
72
76
  return None
73
77
  try:
74
- return sess.windows.get(window_index=str(window))
78
+ return sess.windows.get(window_name=window)
75
79
  except (LibTmuxException, ObjectDoesNotExist):
80
+ # Fallback: if window looks like a legacy index, try index lookup
81
+ if window.isdigit():
82
+ try:
83
+ return sess.windows.get(window_index=window)
84
+ except (LibTmuxException, ObjectDoesNotExist):
85
+ pass
76
86
  return None
77
87
 
78
- def _get_pane(self, session: str, window: int) -> Optional[libtmux.Pane]:
88
+ def _get_pane(self, session: str, window: str) -> Optional[libtmux.Pane]:
79
89
  """Get the first pane of a window, with caching."""
80
90
  cache_key = (session, window)
81
91
  now = time.time()
@@ -94,7 +104,7 @@ class RealTmux:
94
104
  self._pane_cache[cache_key] = (pane, now)
95
105
  return pane
96
106
 
97
- def invalidate_cache(self, session: str = None, window: int = None) -> None:
107
+ def invalidate_cache(self, session: str = None, window: str = None) -> None:
98
108
  """Invalidate cached objects.
99
109
 
100
110
  Args:
@@ -113,7 +123,7 @@ class RealTmux:
113
123
  for k in keys_to_remove:
114
124
  del self._pane_cache[k]
115
125
 
116
- def capture_pane(self, session: str, window: int, lines: int = 100) -> Optional[str]:
126
+ def capture_pane(self, session: str, window: str, lines: int = 100) -> Optional[str]:
117
127
  try:
118
128
  pane = self._get_pane(session, window)
119
129
  if pane is None:
@@ -129,7 +139,7 @@ class RealTmux:
129
139
  self.invalidate_cache(session, window)
130
140
  return None
131
141
 
132
- def send_keys(self, session: str, window: int, keys: str, enter: bool = True) -> bool:
142
+ def send_keys(self, session: str, window: str, keys: str, enter: bool = True) -> bool:
133
143
  try:
134
144
  pane = self._get_pane(session, window)
135
145
  if pane is None:
@@ -188,7 +198,7 @@ class RealTmux:
188
198
  return False
189
199
 
190
200
  def new_window(self, session: str, name: str, command: Optional[List[str]] = None,
191
- cwd: Optional[str] = None) -> Optional[int]:
201
+ cwd: Optional[str] = None) -> Optional[str]:
192
202
  try:
193
203
  sess = self._get_session(session)
194
204
  if sess is None:
@@ -201,11 +211,14 @@ class RealTmux:
201
211
  kwargs['window_shell'] = ' '.join(command)
202
212
 
203
213
  window = sess.new_window(**kwargs)
204
- return int(window.window_index)
214
+ # Prevent tmux from auto-renaming the window based on the
215
+ # running process — we rely on stable window names for lookups.
216
+ window.set_window_option('automatic-rename', 'off')
217
+ return window.window_name
205
218
  except (LibTmuxException, ValueError):
206
219
  return None
207
220
 
208
- def kill_window(self, session: str, window: int) -> bool:
221
+ def kill_window(self, session: str, window: str) -> bool:
209
222
  try:
210
223
  win = self._get_window(session, window)
211
224
  if win is None:
@@ -242,16 +255,18 @@ class RealTmux:
242
255
  except LibTmuxException:
243
256
  return []
244
257
 
245
- def attach(self, session: str, window: Optional[int] = None, bare: bool = False) -> None:
258
+ def attach(self, session: str, window: Optional[str] = None, bare: bool = False) -> None:
246
259
  if bare:
247
260
  self._attach_bare(session, window)
248
261
  else:
249
- target = f"{session}:={window}" if window is not None else session
262
+ from .tmux_utils import tmux_window_target
263
+ target = tmux_window_target(session, window) if window is not None else session
250
264
  os.execlp("tmux", "tmux", "attach-session", "-t", target)
251
265
 
252
- def _attach_bare(self, session: str, window: int) -> None:
266
+ def _attach_bare(self, session: str, window: str) -> None:
253
267
  """Create a linked session with stripped chrome and attach to it."""
254
268
  import subprocess
269
+ from .tmux_utils import tmux_window_target
255
270
 
256
271
  bare_session = f"bare-{session}-{window}"
257
272
 
@@ -271,13 +286,13 @@ class RealTmux:
271
286
  ["tmux", "set", "-t", bare_session, "status", "off"],
272
287
  ["tmux", "set", "-t", bare_session, "mouse", "off"],
273
288
  ["tmux", "set", "-t", bare_session, "destroy-unattached", "on"],
274
- ["tmux", "select-window", "-t", f"{bare_session}:={window}"],
289
+ ["tmux", "select-window", "-t", tmux_window_target(bare_session, window)],
275
290
  ]:
276
291
  subprocess.run(cmd, capture_output=True)
277
292
 
278
293
  os.execlp("tmux", "tmux", "attach-session", "-t", bare_session)
279
294
 
280
- def select_window(self, session: str, window: int) -> bool:
295
+ def select_window(self, session: str, window: str) -> bool:
281
296
  """Select a window in a tmux session (for external pane sync)."""
282
297
  try:
283
298
  win = self._get_window(session, window)