aline-ai 0.7.3__py3-none-any.whl → 0.7.5__py3-none-any.whl
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.
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/METADATA +1 -1
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/RECORD +32 -27
- realign/__init__.py +1 -1
- realign/adapters/codex.py +30 -2
- realign/claude_hooks/stop_hook.py +176 -21
- realign/codex_home.py +71 -0
- realign/codex_hooks/__init__.py +16 -0
- realign/codex_hooks/notify_hook.py +511 -0
- realign/codex_hooks/notify_hook_installer.py +247 -0
- realign/commands/doctor.py +125 -0
- realign/commands/import_shares.py +30 -10
- realign/commands/init.py +16 -0
- realign/commands/sync_agent.py +230 -52
- realign/commands/upgrade.py +117 -0
- realign/commit_pipeline.py +1024 -0
- realign/config.py +3 -11
- realign/dashboard/app.py +150 -0
- realign/dashboard/diagnostics.py +274 -0
- realign/dashboard/screens/create_agent.py +2 -1
- realign/dashboard/screens/create_agent_info.py +40 -77
- realign/dashboard/tmux_manager.py +354 -20
- realign/dashboard/widgets/agents_panel.py +817 -202
- realign/dashboard/widgets/config_panel.py +52 -121
- realign/dashboard/widgets/header.py +1 -1
- realign/db/sqlite_db.py +59 -1
- realign/logging_config.py +51 -6
- realign/watcher_core.py +742 -393
- realign/worker_core.py +206 -15
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/WHEEL +0 -0
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.7.3.dist-info → aline_ai-0.7.5.dist-info}/top_level.txt +0 -0
|
@@ -14,6 +14,7 @@ import shutil
|
|
|
14
14
|
import stat
|
|
15
15
|
import subprocess
|
|
16
16
|
import sys
|
|
17
|
+
import threading
|
|
17
18
|
import time
|
|
18
19
|
import traceback
|
|
19
20
|
import uuid
|
|
@@ -47,6 +48,11 @@ OPT_ATTENTION = "@aline_attention"
|
|
|
47
48
|
OPT_CREATED_AT = "@aline_created_at"
|
|
48
49
|
OPT_NO_TRACK = "@aline_no_track"
|
|
49
50
|
|
|
51
|
+
# Default outer layout preferences (tmux mode).
|
|
52
|
+
# Note: tmux "pixels" are terminal cell columns; Textual has no notion of pixels.
|
|
53
|
+
DEFAULT_DASHBOARD_PANE_WIDTH_COLS = 45
|
|
54
|
+
_STATE_BORDER_RESIZE_ENABLED_KEY = "tmux_border_resize_enabled"
|
|
55
|
+
|
|
50
56
|
|
|
51
57
|
@dataclass(frozen=True)
|
|
52
58
|
class InnerWindow:
|
|
@@ -62,6 +68,9 @@ class InnerWindow:
|
|
|
62
68
|
attention: str | None = None # "permission_request", "stop", or None
|
|
63
69
|
created_at: float | None = None # Unix timestamp when window was created
|
|
64
70
|
no_track: bool = False # Whether tracking is disabled for this terminal
|
|
71
|
+
pane_pid: int | None = None # PID of the initial process in the pane
|
|
72
|
+
pane_current_command: str | None = None # Foreground process in the pane
|
|
73
|
+
pane_tty: str | None = None # Controlling TTY for processes in the pane
|
|
65
74
|
|
|
66
75
|
|
|
67
76
|
def tmux_available() -> bool:
|
|
@@ -93,26 +102,31 @@ def tmux_version() -> tuple[int, int] | None:
|
|
|
93
102
|
return int(match.group(1)), int(match.group(2))
|
|
94
103
|
|
|
95
104
|
|
|
96
|
-
def _run_tmux(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
def _run_tmux(
|
|
106
|
+
args: Sequence[str], *, capture: bool = False, timeout_s: float | None = None
|
|
107
|
+
) -> subprocess.CompletedProcess[str]:
|
|
108
|
+
kwargs: dict[str, object] = {
|
|
109
|
+
"text": True,
|
|
110
|
+
"capture_output": capture,
|
|
111
|
+
"check": False,
|
|
112
|
+
}
|
|
113
|
+
# Keep keyword list minimal for test fakes that don't accept extra kwargs.
|
|
114
|
+
if timeout_s is not None:
|
|
115
|
+
kwargs["timeout"] = timeout_s
|
|
116
|
+
return subprocess.run(["tmux", *args], **kwargs) # type: ignore[arg-type]
|
|
103
117
|
|
|
104
118
|
|
|
105
119
|
def _run_outer_tmux(
|
|
106
|
-
args: Sequence[str], *, capture: bool = False
|
|
120
|
+
args: Sequence[str], *, capture: bool = False, timeout_s: float | None = None
|
|
107
121
|
) -> subprocess.CompletedProcess[str]:
|
|
108
122
|
"""Run tmux commands against the dedicated outer server socket."""
|
|
109
|
-
return _run_tmux(["-L", OUTER_SOCKET, *args], capture=capture)
|
|
123
|
+
return _run_tmux(["-L", OUTER_SOCKET, *args], capture=capture, timeout_s=timeout_s)
|
|
110
124
|
|
|
111
125
|
|
|
112
126
|
def _run_inner_tmux(
|
|
113
|
-
args: Sequence[str], *, capture: bool = False
|
|
127
|
+
args: Sequence[str], *, capture: bool = False, timeout_s: float | None = None
|
|
114
128
|
) -> subprocess.CompletedProcess[str]:
|
|
115
|
-
return _run_tmux(["-L", INNER_SOCKET, *args], capture=capture)
|
|
129
|
+
return _run_tmux(["-L", INNER_SOCKET, *args], capture=capture, timeout_s=timeout_s)
|
|
116
130
|
|
|
117
131
|
|
|
118
132
|
def _python_dashboard_command() -> str:
|
|
@@ -256,6 +270,12 @@ def _load_terminal_state_from_json() -> dict[str, dict[str, str]]:
|
|
|
256
270
|
return {}
|
|
257
271
|
|
|
258
272
|
|
|
273
|
+
_TERMINAL_STATE_CACHE_LOCK = threading.Lock()
|
|
274
|
+
_TERMINAL_STATE_CACHE: dict[str, dict[str, str]] | None = None
|
|
275
|
+
_TERMINAL_STATE_CACHE_AT: float = 0.0
|
|
276
|
+
_TERMINAL_STATE_CACHE_TTL_S: float = 1.5
|
|
277
|
+
|
|
278
|
+
|
|
259
279
|
def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
260
280
|
"""Load terminal state.
|
|
261
281
|
|
|
@@ -265,6 +285,13 @@ def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
|
265
285
|
|
|
266
286
|
Merges both sources, with DB taking precedence.
|
|
267
287
|
"""
|
|
288
|
+
global _TERMINAL_STATE_CACHE, _TERMINAL_STATE_CACHE_AT
|
|
289
|
+
now = time.monotonic()
|
|
290
|
+
with _TERMINAL_STATE_CACHE_LOCK:
|
|
291
|
+
cache = _TERMINAL_STATE_CACHE
|
|
292
|
+
if cache is not None and (now - _TERMINAL_STATE_CACHE_AT) <= _TERMINAL_STATE_CACHE_TTL_S:
|
|
293
|
+
return dict(cache)
|
|
294
|
+
|
|
268
295
|
# Phase 1: Load from database
|
|
269
296
|
db_state = _load_terminal_state_from_db()
|
|
270
297
|
|
|
@@ -275,6 +302,10 @@ def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
|
275
302
|
result = dict(json_state)
|
|
276
303
|
result.update(db_state)
|
|
277
304
|
|
|
305
|
+
with _TERMINAL_STATE_CACHE_LOCK:
|
|
306
|
+
_TERMINAL_STATE_CACHE = dict(result)
|
|
307
|
+
_TERMINAL_STATE_CACHE_AT = time.monotonic()
|
|
308
|
+
|
|
278
309
|
return result
|
|
279
310
|
|
|
280
311
|
|
|
@@ -471,6 +502,13 @@ def bootstrap_dashboard_into_tmux() -> None:
|
|
|
471
502
|
|
|
472
503
|
# Enable mouse for the managed session only.
|
|
473
504
|
_run_outer_tmux(["set-option", "-t", OUTER_SESSION, "mouse", "on"])
|
|
505
|
+
_disable_outer_border_resize()
|
|
506
|
+
try:
|
|
507
|
+
from .state import set_dashboard_state_value
|
|
508
|
+
|
|
509
|
+
set_dashboard_state_value(_STATE_BORDER_RESIZE_ENABLED_KEY, False)
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
474
512
|
|
|
475
513
|
# Disable status bar for cleaner UI (Aline sessions only).
|
|
476
514
|
_run_outer_tmux(["set-option", "-t", OUTER_SESSION, "status", "off"])
|
|
@@ -506,6 +544,9 @@ def bootstrap_dashboard_into_tmux() -> None:
|
|
|
506
544
|
)
|
|
507
545
|
_run_outer_tmux(["select-window", "-t", f"{OUTER_SESSION}:{OUTER_WINDOW}"])
|
|
508
546
|
|
|
547
|
+
# Best-effort: enforce a stable dashboard pane width if the terminal pane already exists.
|
|
548
|
+
enforce_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
549
|
+
|
|
509
550
|
# Sanity-check before exec'ing into tmux attach. If this fails, fall back to non-tmux mode.
|
|
510
551
|
ready = _run_outer_tmux(["has-session", "-t", OUTER_SESSION], capture=True)
|
|
511
552
|
if ready.returncode != 0:
|
|
@@ -691,6 +732,7 @@ def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
|
691
732
|
)
|
|
692
733
|
panes = _parse_lines(panes_out)
|
|
693
734
|
if len(panes) >= 2:
|
|
735
|
+
enforce_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
694
736
|
return True
|
|
695
737
|
|
|
696
738
|
# Split from the dashboard pane to keep it on the left.
|
|
@@ -705,9 +747,157 @@ def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
|
705
747
|
f"{OUTER_SESSION}:{OUTER_WINDOW}.0",
|
|
706
748
|
"-d",
|
|
707
749
|
attach_cmd,
|
|
708
|
-
]
|
|
750
|
+
],
|
|
751
|
+
capture=True,
|
|
709
752
|
)
|
|
710
|
-
|
|
753
|
+
if split.returncode != 0:
|
|
754
|
+
detail = (split.stderr or split.stdout or "").strip()
|
|
755
|
+
if detail:
|
|
756
|
+
logger.warning(f"ensure_right_pane split-window failed: {detail}")
|
|
757
|
+
if split.returncode == 0:
|
|
758
|
+
enforce_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
759
|
+
return True
|
|
760
|
+
return False
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def ensure_right_pane_ready(width_percent: int = 50) -> bool:
|
|
764
|
+
"""Ensure the right pane exists and is attached to the inner tmux session."""
|
|
765
|
+
try:
|
|
766
|
+
ok = ensure_right_pane(width_percent)
|
|
767
|
+
except TypeError:
|
|
768
|
+
# Tests and some callers monkeypatch ensure_right_pane() as a no-arg lambda.
|
|
769
|
+
ok = ensure_right_pane()
|
|
770
|
+
if not ok:
|
|
771
|
+
return False
|
|
772
|
+
|
|
773
|
+
# Best-effort: enforce a stable dashboard pane width whenever we touch the outer layout
|
|
774
|
+
# (unless user enabled drag-to-resize).
|
|
775
|
+
enforce_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
776
|
+
|
|
777
|
+
attach_cmd = shlex.join(["tmux", "-L", INNER_SOCKET, "attach", "-t", INNER_SESSION])
|
|
778
|
+
|
|
779
|
+
# If the right pane exists but isn't running `tmux attach`, it may look "blank" or stale.
|
|
780
|
+
try:
|
|
781
|
+
proc = _run_outer_tmux(
|
|
782
|
+
[
|
|
783
|
+
"display-message",
|
|
784
|
+
"-p",
|
|
785
|
+
"-t",
|
|
786
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}.1",
|
|
787
|
+
"#{pane_current_command}",
|
|
788
|
+
],
|
|
789
|
+
capture=True,
|
|
790
|
+
)
|
|
791
|
+
current_cmd = (proc.stdout or "").strip()
|
|
792
|
+
except Exception:
|
|
793
|
+
current_cmd = ""
|
|
794
|
+
|
|
795
|
+
if current_cmd and current_cmd != "tmux":
|
|
796
|
+
respawn = _run_outer_tmux(
|
|
797
|
+
[
|
|
798
|
+
"respawn-pane",
|
|
799
|
+
"-k",
|
|
800
|
+
"-t",
|
|
801
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}.1",
|
|
802
|
+
attach_cmd,
|
|
803
|
+
],
|
|
804
|
+
capture=True,
|
|
805
|
+
)
|
|
806
|
+
if respawn.returncode != 0:
|
|
807
|
+
detail = (respawn.stderr or respawn.stdout or "").strip()
|
|
808
|
+
if detail:
|
|
809
|
+
logger.warning(f"ensure_right_pane_ready respawn failed: {detail}")
|
|
810
|
+
return False
|
|
811
|
+
|
|
812
|
+
return True
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _disable_outer_border_resize() -> None:
|
|
816
|
+
"""Disable mouse drag-to-resize on pane borders (outer dashboard tmux server only)."""
|
|
817
|
+
try:
|
|
818
|
+
# tmux enables border resizing via MouseDrag1Border. Unbind it on our dedicated server
|
|
819
|
+
# to avoid accidental resizing when selecting text near the pane divider.
|
|
820
|
+
for key in ("MouseDrag1Border", "MouseDown1Border", "MouseDragEnd1Border", "MouseUp1Border"):
|
|
821
|
+
_run_outer_tmux(["unbind-key", "-T", "root", key], capture=True)
|
|
822
|
+
except Exception:
|
|
823
|
+
return
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def outer_border_resize_enabled() -> bool:
|
|
827
|
+
"""Return True when user has enabled tmux border drag-to-resize in the dashboard."""
|
|
828
|
+
try:
|
|
829
|
+
from .state import get_dashboard_state_value
|
|
830
|
+
|
|
831
|
+
return bool(get_dashboard_state_value(_STATE_BORDER_RESIZE_ENABLED_KEY, False))
|
|
832
|
+
except Exception:
|
|
833
|
+
return False
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _set_outer_dashboard_pane_width(width_cols: int) -> None:
|
|
837
|
+
"""Best-effort: size the left dashboard pane to a fixed width (in terminal columns)."""
|
|
838
|
+
try:
|
|
839
|
+
w = int(width_cols)
|
|
840
|
+
if w <= 0:
|
|
841
|
+
return
|
|
842
|
+
except Exception:
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
try:
|
|
846
|
+
# Avoid assuming pane indexes are 0/1. Determine the leftmost pane by geometry.
|
|
847
|
+
panes_out = (
|
|
848
|
+
_run_outer_tmux(
|
|
849
|
+
[
|
|
850
|
+
"list-panes",
|
|
851
|
+
"-t",
|
|
852
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}",
|
|
853
|
+
"-F",
|
|
854
|
+
"#{pane_id}\t#{pane_left}\t#{pane_index}",
|
|
855
|
+
],
|
|
856
|
+
capture=True,
|
|
857
|
+
timeout_s=0.2,
|
|
858
|
+
).stdout
|
|
859
|
+
or ""
|
|
860
|
+
)
|
|
861
|
+
panes: list[tuple[int, int, str]] = []
|
|
862
|
+
for line in _parse_lines(panes_out):
|
|
863
|
+
parts = line.split("\t")
|
|
864
|
+
if len(parts) < 2:
|
|
865
|
+
continue
|
|
866
|
+
pane_id = (parts[0] or "").strip()
|
|
867
|
+
if not pane_id:
|
|
868
|
+
continue
|
|
869
|
+
try:
|
|
870
|
+
pane_left = int((parts[1] or "0").strip())
|
|
871
|
+
except Exception:
|
|
872
|
+
continue
|
|
873
|
+
pane_index = 0
|
|
874
|
+
if len(parts) > 2 and (parts[2] or "").strip():
|
|
875
|
+
try:
|
|
876
|
+
pane_index = int(parts[2].strip())
|
|
877
|
+
except Exception:
|
|
878
|
+
pane_index = 0
|
|
879
|
+
panes.append((pane_left, pane_index, pane_id))
|
|
880
|
+
|
|
881
|
+
# Only enforce widths when there are two panes (dashboard + terminal).
|
|
882
|
+
if len(panes) < 2:
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
panes.sort(key=lambda t: (t[0], t[1]))
|
|
886
|
+
leftmost_pane_id = panes[0][2]
|
|
887
|
+
_run_outer_tmux(
|
|
888
|
+
["resize-pane", "-t", leftmost_pane_id, "-x", str(w)],
|
|
889
|
+
capture=True,
|
|
890
|
+
timeout_s=0.2,
|
|
891
|
+
)
|
|
892
|
+
except Exception:
|
|
893
|
+
return
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def enforce_outer_dashboard_pane_width(width_cols: int = DEFAULT_DASHBOARD_PANE_WIDTH_COLS) -> None:
|
|
897
|
+
"""Best-effort: enforce the fixed dashboard pane width for the outer tmux layout."""
|
|
898
|
+
if outer_border_resize_enabled():
|
|
899
|
+
return
|
|
900
|
+
_set_outer_dashboard_pane_width(width_cols)
|
|
711
901
|
|
|
712
902
|
|
|
713
903
|
def list_inner_windows() -> list[InnerWindow]:
|
|
@@ -745,7 +935,7 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
745
935
|
+ OPT_CREATED_AT
|
|
746
936
|
+ "}\t#{"
|
|
747
937
|
+ OPT_NO_TRACK
|
|
748
|
-
+ "}",
|
|
938
|
+
+ "}\t#{pane_pid}\t#{pane_current_command}\t#{pane_tty}",
|
|
749
939
|
],
|
|
750
940
|
capture=True,
|
|
751
941
|
).stdout
|
|
@@ -775,6 +965,15 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
775
965
|
pass
|
|
776
966
|
no_track_str = parts[11] if len(parts) > 11 and parts[11] else None
|
|
777
967
|
no_track = no_track_str == "1"
|
|
968
|
+
pane_pid_str = parts[12] if len(parts) > 12 and parts[12] else None
|
|
969
|
+
pane_pid: int | None = None
|
|
970
|
+
if pane_pid_str:
|
|
971
|
+
try:
|
|
972
|
+
pane_pid = int(pane_pid_str)
|
|
973
|
+
except ValueError:
|
|
974
|
+
pass
|
|
975
|
+
pane_current_command = parts[13] if len(parts) > 13 and parts[13] else None
|
|
976
|
+
pane_tty = parts[14] if len(parts) > 14 and parts[14] else None
|
|
778
977
|
|
|
779
978
|
if terminal_id:
|
|
780
979
|
persisted = state.get(terminal_id) or {}
|
|
@@ -833,6 +1032,9 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
833
1032
|
attention=attention,
|
|
834
1033
|
created_at=created_at,
|
|
835
1034
|
no_track=no_track,
|
|
1035
|
+
pane_pid=pane_pid,
|
|
1036
|
+
pane_current_command=pane_current_command,
|
|
1037
|
+
pane_tty=pane_tty,
|
|
836
1038
|
)
|
|
837
1039
|
)
|
|
838
1040
|
# Sort by creation time (newest first). Windows without created_at go to the bottom.
|
|
@@ -840,6 +1042,33 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
840
1042
|
return windows
|
|
841
1043
|
|
|
842
1044
|
|
|
1045
|
+
def list_outer_panes(*, timeout_s: float = 0.2) -> list[str]:
|
|
1046
|
+
"""List panes in the outer dashboard window (best-effort).
|
|
1047
|
+
|
|
1048
|
+
Intended for lightweight watchdog checks; returns tab-delimited lines in the same
|
|
1049
|
+
format as `collect_tmux_debug_state()["outer_panes"]["stdout"]`.
|
|
1050
|
+
"""
|
|
1051
|
+
if not tmux_available():
|
|
1052
|
+
return []
|
|
1053
|
+
try:
|
|
1054
|
+
proc = _run_outer_tmux(
|
|
1055
|
+
[
|
|
1056
|
+
"list-panes",
|
|
1057
|
+
"-t",
|
|
1058
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}",
|
|
1059
|
+
"-F",
|
|
1060
|
+
"#{pane_index}\t#{pane_active}\t#{pane_current_command}\t#{pane_pid}\t#{pane_tty}",
|
|
1061
|
+
],
|
|
1062
|
+
capture=True,
|
|
1063
|
+
timeout_s=timeout_s,
|
|
1064
|
+
)
|
|
1065
|
+
except Exception:
|
|
1066
|
+
return []
|
|
1067
|
+
if proc.returncode != 0:
|
|
1068
|
+
return []
|
|
1069
|
+
return [ln for ln in (proc.stdout or "").splitlines() if ln.strip()]
|
|
1070
|
+
|
|
1071
|
+
|
|
843
1072
|
def set_inner_window_options(window_id: str, options: dict[str, str]) -> bool:
|
|
844
1073
|
import time as _time
|
|
845
1074
|
|
|
@@ -874,7 +1103,8 @@ def create_inner_window(
|
|
|
874
1103
|
|
|
875
1104
|
t0 = _time.time()
|
|
876
1105
|
logger.debug("[PERF] create_inner_window START")
|
|
877
|
-
if not
|
|
1106
|
+
if not ensure_right_pane_ready():
|
|
1107
|
+
logger.warning("create_inner_window: right pane unavailable")
|
|
878
1108
|
return None
|
|
879
1109
|
logger.debug(f"[PERF] create_inner_window ensure_right_pane: {_time.time() - t0:.3f}s")
|
|
880
1110
|
|
|
@@ -903,6 +1133,9 @@ def create_inner_window(
|
|
|
903
1133
|
)
|
|
904
1134
|
logger.debug(f"[PERF] create_inner_window new-window: {_time.time() - t2:.3f}s")
|
|
905
1135
|
if proc.returncode != 0:
|
|
1136
|
+
detail = (proc.stderr or proc.stdout or "").strip()
|
|
1137
|
+
if detail:
|
|
1138
|
+
logger.warning(f"create_inner_window new-window failed: {detail}")
|
|
906
1139
|
return None
|
|
907
1140
|
|
|
908
1141
|
created = _parse_lines(proc.stdout or "")
|
|
@@ -943,16 +1176,26 @@ def create_inner_window(
|
|
|
943
1176
|
|
|
944
1177
|
|
|
945
1178
|
def select_inner_window(window_id: str) -> bool:
|
|
946
|
-
if not
|
|
1179
|
+
if not ensure_right_pane_ready():
|
|
947
1180
|
return False
|
|
948
|
-
|
|
1181
|
+
proc = _run_inner_tmux(["select-window", "-t", window_id], capture=True)
|
|
1182
|
+
if proc.returncode != 0:
|
|
1183
|
+
detail = (proc.stderr or proc.stdout or "").strip()
|
|
1184
|
+
if detail:
|
|
1185
|
+
logger.warning(f"select_inner_window failed ({window_id}): {detail}")
|
|
1186
|
+
return False
|
|
1187
|
+
return True
|
|
949
1188
|
|
|
950
1189
|
|
|
951
1190
|
def focus_right_pane() -> bool:
|
|
952
1191
|
"""Focus the right pane (terminal area) in the outer tmux layout."""
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1192
|
+
proc = _run_outer_tmux(["select-pane", "-t", f"{OUTER_SESSION}:{OUTER_WINDOW}.1"], capture=True)
|
|
1193
|
+
if proc.returncode != 0:
|
|
1194
|
+
detail = (proc.stderr or proc.stdout or "").strip()
|
|
1195
|
+
if detail:
|
|
1196
|
+
logger.warning(f"focus_right_pane failed: {detail}")
|
|
1197
|
+
return False
|
|
1198
|
+
return True
|
|
956
1199
|
|
|
957
1200
|
|
|
958
1201
|
def clear_attention(window_id: str) -> bool:
|
|
@@ -992,3 +1235,94 @@ def get_active_context_id(*, allowed_providers: set[str] | None = None) -> str |
|
|
|
992
1235
|
|
|
993
1236
|
context_id = (active.context_id or "").strip()
|
|
994
1237
|
return context_id or None
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def collect_tmux_debug_state() -> dict[str, object]:
|
|
1241
|
+
"""Best-effort snapshot of tmux state for diagnosing blank/stuck panes.
|
|
1242
|
+
|
|
1243
|
+
This must be non-intrusive: it should not create sessions or change state.
|
|
1244
|
+
"""
|
|
1245
|
+
|
|
1246
|
+
def _cap(proc: subprocess.CompletedProcess[str] | None) -> dict[str, object]:
|
|
1247
|
+
if proc is None:
|
|
1248
|
+
return {}
|
|
1249
|
+
|
|
1250
|
+
def _trim(s: str | None) -> str:
|
|
1251
|
+
text = (s or "").strip()
|
|
1252
|
+
if len(text) > 4000:
|
|
1253
|
+
return text[:4000] + "…(truncated)"
|
|
1254
|
+
return text
|
|
1255
|
+
|
|
1256
|
+
return {
|
|
1257
|
+
"rc": int(getattr(proc, "returncode", -1)),
|
|
1258
|
+
"stdout": _trim(getattr(proc, "stdout", "")),
|
|
1259
|
+
"stderr": _trim(getattr(proc, "stderr", "")),
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
state: dict[str, object] = {
|
|
1263
|
+
"tmux_available": tmux_available(),
|
|
1264
|
+
"in_tmux": in_tmux(),
|
|
1265
|
+
"managed_env": managed_env_enabled(),
|
|
1266
|
+
"outer_socket": OUTER_SOCKET,
|
|
1267
|
+
"inner_socket": INNER_SOCKET,
|
|
1268
|
+
"outer_session": OUTER_SESSION,
|
|
1269
|
+
"outer_window": OUTER_WINDOW,
|
|
1270
|
+
"inner_session": INNER_SESSION,
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if not tmux_available():
|
|
1274
|
+
return state
|
|
1275
|
+
|
|
1276
|
+
try:
|
|
1277
|
+
state["outer_has_session"] = _cap(
|
|
1278
|
+
_run_outer_tmux(["has-session", "-t", OUTER_SESSION], capture=True, timeout_s=0.5)
|
|
1279
|
+
)
|
|
1280
|
+
state["outer_panes"] = _cap(
|
|
1281
|
+
_run_outer_tmux(
|
|
1282
|
+
[
|
|
1283
|
+
"list-panes",
|
|
1284
|
+
"-t",
|
|
1285
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}",
|
|
1286
|
+
"-F",
|
|
1287
|
+
"#{pane_index}\t#{pane_active}\t#{pane_current_command}\t#{pane_pid}\t#{pane_tty}",
|
|
1288
|
+
],
|
|
1289
|
+
capture=True,
|
|
1290
|
+
timeout_s=0.5,
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1293
|
+
except Exception:
|
|
1294
|
+
pass
|
|
1295
|
+
|
|
1296
|
+
try:
|
|
1297
|
+
state["inner_has_session"] = _cap(
|
|
1298
|
+
_run_inner_tmux(["has-session", "-t", INNER_SESSION], capture=True, timeout_s=0.5)
|
|
1299
|
+
)
|
|
1300
|
+
state["inner_windows"] = _cap(
|
|
1301
|
+
_run_inner_tmux(
|
|
1302
|
+
[
|
|
1303
|
+
"list-windows",
|
|
1304
|
+
"-t",
|
|
1305
|
+
INNER_SESSION,
|
|
1306
|
+
"-F",
|
|
1307
|
+
"#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}\t#{"
|
|
1308
|
+
+ OPT_TERMINAL_ID
|
|
1309
|
+
+ "}\t#{"
|
|
1310
|
+
+ OPT_PROVIDER
|
|
1311
|
+
+ "}\t#{"
|
|
1312
|
+
+ OPT_SESSION_TYPE
|
|
1313
|
+
+ "}\t#{"
|
|
1314
|
+
+ OPT_SESSION_ID
|
|
1315
|
+
+ "}\t#{"
|
|
1316
|
+
+ OPT_CONTEXT_ID
|
|
1317
|
+
+ "}\t#{"
|
|
1318
|
+
+ OPT_ATTENTION
|
|
1319
|
+
+ "}",
|
|
1320
|
+
],
|
|
1321
|
+
capture=True,
|
|
1322
|
+
timeout_s=0.5,
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
1325
|
+
except Exception:
|
|
1326
|
+
pass
|
|
1327
|
+
|
|
1328
|
+
return state
|