aline-ai 0.7.2__py3-none-any.whl → 0.7.4__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.2.dist-info → aline_ai-0.7.4.dist-info}/METADATA +1 -1
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.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/export_shares.py +188 -65
- realign/commands/import_shares.py +30 -10
- realign/commands/init.py +16 -0
- realign/commands/sync_agent.py +274 -44
- realign/commit_pipeline.py +1024 -0
- realign/config.py +3 -11
- realign/dashboard/app.py +151 -2
- 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 +348 -33
- realign/dashboard/widgets/agents_panel.py +942 -314
- realign/dashboard/widgets/config_panel.py +34 -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.2.dist-info → aline_ai-0.7.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.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,10 @@ 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
|
+
|
|
50
55
|
|
|
51
56
|
@dataclass(frozen=True)
|
|
52
57
|
class InnerWindow:
|
|
@@ -62,6 +67,9 @@ class InnerWindow:
|
|
|
62
67
|
attention: str | None = None # "permission_request", "stop", or None
|
|
63
68
|
created_at: float | None = None # Unix timestamp when window was created
|
|
64
69
|
no_track: bool = False # Whether tracking is disabled for this terminal
|
|
70
|
+
pane_pid: int | None = None # PID of the initial process in the pane
|
|
71
|
+
pane_current_command: str | None = None # Foreground process in the pane
|
|
72
|
+
pane_tty: str | None = None # Controlling TTY for processes in the pane
|
|
65
73
|
|
|
66
74
|
|
|
67
75
|
def tmux_available() -> bool:
|
|
@@ -93,26 +101,31 @@ def tmux_version() -> tuple[int, int] | None:
|
|
|
93
101
|
return int(match.group(1)), int(match.group(2))
|
|
94
102
|
|
|
95
103
|
|
|
96
|
-
def _run_tmux(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
def _run_tmux(
|
|
105
|
+
args: Sequence[str], *, capture: bool = False, timeout_s: float | None = None
|
|
106
|
+
) -> subprocess.CompletedProcess[str]:
|
|
107
|
+
kwargs: dict[str, object] = {
|
|
108
|
+
"text": True,
|
|
109
|
+
"capture_output": capture,
|
|
110
|
+
"check": False,
|
|
111
|
+
}
|
|
112
|
+
# Keep keyword list minimal for test fakes that don't accept extra kwargs.
|
|
113
|
+
if timeout_s is not None:
|
|
114
|
+
kwargs["timeout"] = timeout_s
|
|
115
|
+
return subprocess.run(["tmux", *args], **kwargs) # type: ignore[arg-type]
|
|
103
116
|
|
|
104
117
|
|
|
105
118
|
def _run_outer_tmux(
|
|
106
|
-
args: Sequence[str], *, capture: bool = False
|
|
119
|
+
args: Sequence[str], *, capture: bool = False, timeout_s: float | None = None
|
|
107
120
|
) -> subprocess.CompletedProcess[str]:
|
|
108
121
|
"""Run tmux commands against the dedicated outer server socket."""
|
|
109
|
-
return _run_tmux(["-L", OUTER_SOCKET, *args], capture=capture)
|
|
122
|
+
return _run_tmux(["-L", OUTER_SOCKET, *args], capture=capture, timeout_s=timeout_s)
|
|
110
123
|
|
|
111
124
|
|
|
112
125
|
def _run_inner_tmux(
|
|
113
|
-
args: Sequence[str], *, capture: bool = False
|
|
126
|
+
args: Sequence[str], *, capture: bool = False, timeout_s: float | None = None
|
|
114
127
|
) -> subprocess.CompletedProcess[str]:
|
|
115
|
-
return _run_tmux(["-L", INNER_SOCKET, *args], capture=capture)
|
|
128
|
+
return _run_tmux(["-L", INNER_SOCKET, *args], capture=capture, timeout_s=timeout_s)
|
|
116
129
|
|
|
117
130
|
|
|
118
131
|
def _python_dashboard_command() -> str:
|
|
@@ -196,16 +209,17 @@ def _session_id_from_transcript_path(transcript_path: str | None) -> str | None:
|
|
|
196
209
|
def _load_terminal_state_from_db() -> dict[str, dict[str, str]]:
|
|
197
210
|
"""Load terminal state from database (best-effort)."""
|
|
198
211
|
import time as _time
|
|
212
|
+
|
|
199
213
|
t0 = _time.time()
|
|
200
214
|
try:
|
|
201
215
|
from ..db import get_database
|
|
202
216
|
|
|
203
217
|
t1 = _time.time()
|
|
204
218
|
db = get_database(read_only=True)
|
|
205
|
-
logger.
|
|
219
|
+
logger.debug(f"[PERF] _load_terminal_state_from_db get_database: {_time.time() - t1:.3f}s")
|
|
206
220
|
t2 = _time.time()
|
|
207
221
|
agents = db.list_agents(status="active", limit=100)
|
|
208
|
-
logger.
|
|
222
|
+
logger.debug(f"[PERF] _load_terminal_state_from_db list_agents: {_time.time() - t2:.3f}s")
|
|
209
223
|
|
|
210
224
|
out: dict[str, dict[str, str]] = {}
|
|
211
225
|
for agent in agents:
|
|
@@ -255,6 +269,12 @@ def _load_terminal_state_from_json() -> dict[str, dict[str, str]]:
|
|
|
255
269
|
return {}
|
|
256
270
|
|
|
257
271
|
|
|
272
|
+
_TERMINAL_STATE_CACHE_LOCK = threading.Lock()
|
|
273
|
+
_TERMINAL_STATE_CACHE: dict[str, dict[str, str]] | None = None
|
|
274
|
+
_TERMINAL_STATE_CACHE_AT: float = 0.0
|
|
275
|
+
_TERMINAL_STATE_CACHE_TTL_S: float = 1.5
|
|
276
|
+
|
|
277
|
+
|
|
258
278
|
def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
259
279
|
"""Load terminal state.
|
|
260
280
|
|
|
@@ -264,6 +284,13 @@ def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
|
264
284
|
|
|
265
285
|
Merges both sources, with DB taking precedence.
|
|
266
286
|
"""
|
|
287
|
+
global _TERMINAL_STATE_CACHE, _TERMINAL_STATE_CACHE_AT
|
|
288
|
+
now = time.monotonic()
|
|
289
|
+
with _TERMINAL_STATE_CACHE_LOCK:
|
|
290
|
+
cache = _TERMINAL_STATE_CACHE
|
|
291
|
+
if cache is not None and (now - _TERMINAL_STATE_CACHE_AT) <= _TERMINAL_STATE_CACHE_TTL_S:
|
|
292
|
+
return dict(cache)
|
|
293
|
+
|
|
267
294
|
# Phase 1: Load from database
|
|
268
295
|
db_state = _load_terminal_state_from_db()
|
|
269
296
|
|
|
@@ -274,6 +301,10 @@ def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
|
274
301
|
result = dict(json_state)
|
|
275
302
|
result.update(db_state)
|
|
276
303
|
|
|
304
|
+
with _TERMINAL_STATE_CACHE_LOCK:
|
|
305
|
+
_TERMINAL_STATE_CACHE = dict(result)
|
|
306
|
+
_TERMINAL_STATE_CACHE_AT = time.monotonic()
|
|
307
|
+
|
|
277
308
|
return result
|
|
278
309
|
|
|
279
310
|
|
|
@@ -470,6 +501,7 @@ def bootstrap_dashboard_into_tmux() -> None:
|
|
|
470
501
|
|
|
471
502
|
# Enable mouse for the managed session only.
|
|
472
503
|
_run_outer_tmux(["set-option", "-t", OUTER_SESSION, "mouse", "on"])
|
|
504
|
+
_disable_outer_border_resize()
|
|
473
505
|
|
|
474
506
|
# Disable status bar for cleaner UI (Aline sessions only).
|
|
475
507
|
_run_outer_tmux(["set-option", "-t", OUTER_SESSION, "status", "off"])
|
|
@@ -505,6 +537,9 @@ def bootstrap_dashboard_into_tmux() -> None:
|
|
|
505
537
|
)
|
|
506
538
|
_run_outer_tmux(["select-window", "-t", f"{OUTER_SESSION}:{OUTER_WINDOW}"])
|
|
507
539
|
|
|
540
|
+
# Best-effort: enforce a stable dashboard pane width if the terminal pane already exists.
|
|
541
|
+
_set_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
542
|
+
|
|
508
543
|
# Sanity-check before exec'ing into tmux attach. If this fails, fall back to non-tmux mode.
|
|
509
544
|
ready = _run_outer_tmux(["has-session", "-t", OUTER_SESSION], capture=True)
|
|
510
545
|
if ready.returncode != 0:
|
|
@@ -690,6 +725,7 @@ def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
|
690
725
|
)
|
|
691
726
|
panes = _parse_lines(panes_out)
|
|
692
727
|
if len(panes) >= 2:
|
|
728
|
+
_set_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
693
729
|
return True
|
|
694
730
|
|
|
695
731
|
# Split from the dashboard pane to keep it on the left.
|
|
@@ -704,20 +740,156 @@ def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
|
704
740
|
f"{OUTER_SESSION}:{OUTER_WINDOW}.0",
|
|
705
741
|
"-d",
|
|
706
742
|
attach_cmd,
|
|
707
|
-
]
|
|
743
|
+
],
|
|
744
|
+
capture=True,
|
|
708
745
|
)
|
|
709
|
-
|
|
746
|
+
if split.returncode != 0:
|
|
747
|
+
detail = (split.stderr or split.stdout or "").strip()
|
|
748
|
+
if detail:
|
|
749
|
+
logger.warning(f"ensure_right_pane split-window failed: {detail}")
|
|
750
|
+
if split.returncode == 0:
|
|
751
|
+
_set_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
752
|
+
return True
|
|
753
|
+
return False
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def ensure_right_pane_ready(width_percent: int = 50) -> bool:
|
|
757
|
+
"""Ensure the right pane exists and is attached to the inner tmux session."""
|
|
758
|
+
try:
|
|
759
|
+
ok = ensure_right_pane(width_percent)
|
|
760
|
+
except TypeError:
|
|
761
|
+
# Tests and some callers monkeypatch ensure_right_pane() as a no-arg lambda.
|
|
762
|
+
ok = ensure_right_pane()
|
|
763
|
+
if not ok:
|
|
764
|
+
return False
|
|
765
|
+
|
|
766
|
+
# Best-effort: enforce a stable dashboard pane width whenever we touch the outer layout.
|
|
767
|
+
_set_outer_dashboard_pane_width(DEFAULT_DASHBOARD_PANE_WIDTH_COLS)
|
|
768
|
+
|
|
769
|
+
attach_cmd = shlex.join(["tmux", "-L", INNER_SOCKET, "attach", "-t", INNER_SESSION])
|
|
770
|
+
|
|
771
|
+
# If the right pane exists but isn't running `tmux attach`, it may look "blank" or stale.
|
|
772
|
+
try:
|
|
773
|
+
proc = _run_outer_tmux(
|
|
774
|
+
[
|
|
775
|
+
"display-message",
|
|
776
|
+
"-p",
|
|
777
|
+
"-t",
|
|
778
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}.1",
|
|
779
|
+
"#{pane_current_command}",
|
|
780
|
+
],
|
|
781
|
+
capture=True,
|
|
782
|
+
)
|
|
783
|
+
current_cmd = (proc.stdout or "").strip()
|
|
784
|
+
except Exception:
|
|
785
|
+
current_cmd = ""
|
|
786
|
+
|
|
787
|
+
if current_cmd and current_cmd != "tmux":
|
|
788
|
+
respawn = _run_outer_tmux(
|
|
789
|
+
[
|
|
790
|
+
"respawn-pane",
|
|
791
|
+
"-k",
|
|
792
|
+
"-t",
|
|
793
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}.1",
|
|
794
|
+
attach_cmd,
|
|
795
|
+
],
|
|
796
|
+
capture=True,
|
|
797
|
+
)
|
|
798
|
+
if respawn.returncode != 0:
|
|
799
|
+
detail = (respawn.stderr or respawn.stdout or "").strip()
|
|
800
|
+
if detail:
|
|
801
|
+
logger.warning(f"ensure_right_pane_ready respawn failed: {detail}")
|
|
802
|
+
return False
|
|
803
|
+
|
|
804
|
+
return True
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _disable_outer_border_resize() -> None:
|
|
808
|
+
"""Disable mouse drag-to-resize on pane borders (outer dashboard tmux server only)."""
|
|
809
|
+
try:
|
|
810
|
+
# tmux enables border resizing via MouseDrag1Border. Unbind it on our dedicated server
|
|
811
|
+
# to avoid accidental resizing when selecting text near the pane divider.
|
|
812
|
+
for key in ("MouseDrag1Border", "MouseDown1Border", "MouseDragEnd1Border", "MouseUp1Border"):
|
|
813
|
+
_run_outer_tmux(["unbind-key", "-T", "root", key], capture=True)
|
|
814
|
+
except Exception:
|
|
815
|
+
return
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _set_outer_dashboard_pane_width(width_cols: int) -> None:
|
|
819
|
+
"""Best-effort: size the left dashboard pane to a fixed width (in terminal columns)."""
|
|
820
|
+
try:
|
|
821
|
+
w = int(width_cols)
|
|
822
|
+
if w <= 0:
|
|
823
|
+
return
|
|
824
|
+
except Exception:
|
|
825
|
+
return
|
|
826
|
+
|
|
827
|
+
try:
|
|
828
|
+
# Avoid assuming pane indexes are 0/1. Determine the leftmost pane by geometry.
|
|
829
|
+
panes_out = (
|
|
830
|
+
_run_outer_tmux(
|
|
831
|
+
[
|
|
832
|
+
"list-panes",
|
|
833
|
+
"-t",
|
|
834
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}",
|
|
835
|
+
"-F",
|
|
836
|
+
"#{pane_id}\t#{pane_left}\t#{pane_index}",
|
|
837
|
+
],
|
|
838
|
+
capture=True,
|
|
839
|
+
timeout_s=0.2,
|
|
840
|
+
).stdout
|
|
841
|
+
or ""
|
|
842
|
+
)
|
|
843
|
+
panes: list[tuple[int, int, str]] = []
|
|
844
|
+
for line in _parse_lines(panes_out):
|
|
845
|
+
parts = line.split("\t")
|
|
846
|
+
if len(parts) < 2:
|
|
847
|
+
continue
|
|
848
|
+
pane_id = (parts[0] or "").strip()
|
|
849
|
+
if not pane_id:
|
|
850
|
+
continue
|
|
851
|
+
try:
|
|
852
|
+
pane_left = int((parts[1] or "0").strip())
|
|
853
|
+
except Exception:
|
|
854
|
+
continue
|
|
855
|
+
pane_index = 0
|
|
856
|
+
if len(parts) > 2 and (parts[2] or "").strip():
|
|
857
|
+
try:
|
|
858
|
+
pane_index = int(parts[2].strip())
|
|
859
|
+
except Exception:
|
|
860
|
+
pane_index = 0
|
|
861
|
+
panes.append((pane_left, pane_index, pane_id))
|
|
862
|
+
|
|
863
|
+
# Only enforce widths when there are two panes (dashboard + terminal).
|
|
864
|
+
if len(panes) < 2:
|
|
865
|
+
return
|
|
866
|
+
|
|
867
|
+
panes.sort(key=lambda t: (t[0], t[1]))
|
|
868
|
+
leftmost_pane_id = panes[0][2]
|
|
869
|
+
_run_outer_tmux(
|
|
870
|
+
["resize-pane", "-t", leftmost_pane_id, "-x", str(w)],
|
|
871
|
+
capture=True,
|
|
872
|
+
timeout_s=0.2,
|
|
873
|
+
)
|
|
874
|
+
except Exception:
|
|
875
|
+
return
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def enforce_outer_dashboard_pane_width(width_cols: int = DEFAULT_DASHBOARD_PANE_WIDTH_COLS) -> None:
|
|
879
|
+
"""Best-effort: enforce the fixed dashboard pane width for the outer tmux layout."""
|
|
880
|
+
_set_outer_dashboard_pane_width(width_cols)
|
|
710
881
|
|
|
711
882
|
|
|
712
883
|
def list_inner_windows() -> list[InnerWindow]:
|
|
713
884
|
import time as _time
|
|
885
|
+
|
|
714
886
|
t0 = _time.time()
|
|
715
887
|
if not ensure_inner_session():
|
|
716
888
|
return []
|
|
717
|
-
logger.
|
|
889
|
+
logger.debug(f"[PERF] list_inner_windows ensure_inner_session: {_time.time() - t0:.3f}s")
|
|
718
890
|
t1 = _time.time()
|
|
719
891
|
state = _load_terminal_state()
|
|
720
|
-
logger.
|
|
892
|
+
logger.debug(f"[PERF] list_inner_windows _load_terminal_state: {_time.time() - t1:.3f}s")
|
|
721
893
|
out = (
|
|
722
894
|
_run_inner_tmux(
|
|
723
895
|
[
|
|
@@ -743,7 +915,7 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
743
915
|
+ OPT_CREATED_AT
|
|
744
916
|
+ "}\t#{"
|
|
745
917
|
+ OPT_NO_TRACK
|
|
746
|
-
+ "}",
|
|
918
|
+
+ "}\t#{pane_pid}\t#{pane_current_command}\t#{pane_tty}",
|
|
747
919
|
],
|
|
748
920
|
capture=True,
|
|
749
921
|
).stdout
|
|
@@ -773,6 +945,15 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
773
945
|
pass
|
|
774
946
|
no_track_str = parts[11] if len(parts) > 11 and parts[11] else None
|
|
775
947
|
no_track = no_track_str == "1"
|
|
948
|
+
pane_pid_str = parts[12] if len(parts) > 12 and parts[12] else None
|
|
949
|
+
pane_pid: int | None = None
|
|
950
|
+
if pane_pid_str:
|
|
951
|
+
try:
|
|
952
|
+
pane_pid = int(pane_pid_str)
|
|
953
|
+
except ValueError:
|
|
954
|
+
pass
|
|
955
|
+
pane_current_command = parts[13] if len(parts) > 13 and parts[13] else None
|
|
956
|
+
pane_tty = parts[14] if len(parts) > 14 and parts[14] else None
|
|
776
957
|
|
|
777
958
|
if terminal_id:
|
|
778
959
|
persisted = state.get(terminal_id) or {}
|
|
@@ -831,6 +1012,9 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
831
1012
|
attention=attention,
|
|
832
1013
|
created_at=created_at,
|
|
833
1014
|
no_track=no_track,
|
|
1015
|
+
pane_pid=pane_pid,
|
|
1016
|
+
pane_current_command=pane_current_command,
|
|
1017
|
+
pane_tty=pane_tty,
|
|
834
1018
|
)
|
|
835
1019
|
)
|
|
836
1020
|
# Sort by creation time (newest first). Windows without created_at go to the bottom.
|
|
@@ -838,8 +1022,36 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
838
1022
|
return windows
|
|
839
1023
|
|
|
840
1024
|
|
|
1025
|
+
def list_outer_panes(*, timeout_s: float = 0.2) -> list[str]:
|
|
1026
|
+
"""List panes in the outer dashboard window (best-effort).
|
|
1027
|
+
|
|
1028
|
+
Intended for lightweight watchdog checks; returns tab-delimited lines in the same
|
|
1029
|
+
format as `collect_tmux_debug_state()["outer_panes"]["stdout"]`.
|
|
1030
|
+
"""
|
|
1031
|
+
if not tmux_available():
|
|
1032
|
+
return []
|
|
1033
|
+
try:
|
|
1034
|
+
proc = _run_outer_tmux(
|
|
1035
|
+
[
|
|
1036
|
+
"list-panes",
|
|
1037
|
+
"-t",
|
|
1038
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}",
|
|
1039
|
+
"-F",
|
|
1040
|
+
"#{pane_index}\t#{pane_active}\t#{pane_current_command}\t#{pane_pid}\t#{pane_tty}",
|
|
1041
|
+
],
|
|
1042
|
+
capture=True,
|
|
1043
|
+
timeout_s=timeout_s,
|
|
1044
|
+
)
|
|
1045
|
+
except Exception:
|
|
1046
|
+
return []
|
|
1047
|
+
if proc.returncode != 0:
|
|
1048
|
+
return []
|
|
1049
|
+
return [ln for ln in (proc.stdout or "").splitlines() if ln.strip()]
|
|
1050
|
+
|
|
1051
|
+
|
|
841
1052
|
def set_inner_window_options(window_id: str, options: dict[str, str]) -> bool:
|
|
842
1053
|
import time as _time
|
|
1054
|
+
|
|
843
1055
|
if not ensure_inner_session():
|
|
844
1056
|
return False
|
|
845
1057
|
ok = True
|
|
@@ -848,7 +1060,7 @@ def set_inner_window_options(window_id: str, options: dict[str, str]) -> bool:
|
|
|
848
1060
|
# Important: these are per-window (not session-wide) to avoid cross-tab clobbering.
|
|
849
1061
|
if _run_inner_tmux(["set-option", "-w", "-t", window_id, key, value]).returncode != 0:
|
|
850
1062
|
ok = False
|
|
851
|
-
logger.
|
|
1063
|
+
logger.debug(f"[PERF] set_inner_window_options {key}: {_time.time() - t0:.3f}s")
|
|
852
1064
|
return ok
|
|
853
1065
|
|
|
854
1066
|
|
|
@@ -868,15 +1080,17 @@ def create_inner_window(
|
|
|
868
1080
|
no_track: bool = False,
|
|
869
1081
|
) -> InnerWindow | None:
|
|
870
1082
|
import time as _time
|
|
1083
|
+
|
|
871
1084
|
t0 = _time.time()
|
|
872
|
-
logger.
|
|
873
|
-
if not
|
|
1085
|
+
logger.debug("[PERF] create_inner_window START")
|
|
1086
|
+
if not ensure_right_pane_ready():
|
|
1087
|
+
logger.warning("create_inner_window: right pane unavailable")
|
|
874
1088
|
return None
|
|
875
|
-
logger.
|
|
1089
|
+
logger.debug(f"[PERF] create_inner_window ensure_right_pane: {_time.time() - t0:.3f}s")
|
|
876
1090
|
|
|
877
1091
|
t1 = _time.time()
|
|
878
1092
|
existing = list_inner_windows()
|
|
879
|
-
logger.
|
|
1093
|
+
logger.debug(f"[PERF] create_inner_window list_inner_windows: {_time.time() - t1:.3f}s")
|
|
880
1094
|
name = _unique_name((w.window_name for w in existing), base_name)
|
|
881
1095
|
|
|
882
1096
|
# Record creation time before creating the window
|
|
@@ -897,8 +1111,11 @@ def create_inner_window(
|
|
|
897
1111
|
],
|
|
898
1112
|
capture=True,
|
|
899
1113
|
)
|
|
900
|
-
logger.
|
|
1114
|
+
logger.debug(f"[PERF] create_inner_window new-window: {_time.time() - t2:.3f}s")
|
|
901
1115
|
if proc.returncode != 0:
|
|
1116
|
+
detail = (proc.stderr or proc.stdout or "").strip()
|
|
1117
|
+
if detail:
|
|
1118
|
+
logger.warning(f"create_inner_window new-window failed: {detail}")
|
|
902
1119
|
return None
|
|
903
1120
|
|
|
904
1121
|
created = _parse_lines(proc.stdout or "")
|
|
@@ -923,7 +1140,7 @@ def create_inner_window(
|
|
|
923
1140
|
opts.setdefault(OPT_NO_TRACK, "")
|
|
924
1141
|
t3 = _time.time()
|
|
925
1142
|
set_inner_window_options(window_id, opts)
|
|
926
|
-
logger.
|
|
1143
|
+
logger.debug(f"[PERF] create_inner_window set_options: {_time.time() - t3:.3f}s")
|
|
927
1144
|
|
|
928
1145
|
_run_inner_tmux(["select-window", "-t", window_id])
|
|
929
1146
|
|
|
@@ -939,19 +1156,26 @@ def create_inner_window(
|
|
|
939
1156
|
|
|
940
1157
|
|
|
941
1158
|
def select_inner_window(window_id: str) -> bool:
|
|
942
|
-
if not
|
|
1159
|
+
if not ensure_right_pane_ready():
|
|
1160
|
+
return False
|
|
1161
|
+
proc = _run_inner_tmux(["select-window", "-t", window_id], capture=True)
|
|
1162
|
+
if proc.returncode != 0:
|
|
1163
|
+
detail = (proc.stderr or proc.stdout or "").strip()
|
|
1164
|
+
if detail:
|
|
1165
|
+
logger.warning(f"select_inner_window failed ({window_id}): {detail}")
|
|
943
1166
|
return False
|
|
944
|
-
return
|
|
1167
|
+
return True
|
|
945
1168
|
|
|
946
1169
|
|
|
947
1170
|
def focus_right_pane() -> bool:
|
|
948
1171
|
"""Focus the right pane (terminal area) in the outer tmux layout."""
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1172
|
+
proc = _run_outer_tmux(["select-pane", "-t", f"{OUTER_SESSION}:{OUTER_WINDOW}.1"], capture=True)
|
|
1173
|
+
if proc.returncode != 0:
|
|
1174
|
+
detail = (proc.stderr or proc.stdout or "").strip()
|
|
1175
|
+
if detail:
|
|
1176
|
+
logger.warning(f"focus_right_pane failed: {detail}")
|
|
1177
|
+
return False
|
|
1178
|
+
return True
|
|
955
1179
|
|
|
956
1180
|
|
|
957
1181
|
def clear_attention(window_id: str) -> bool:
|
|
@@ -991,3 +1215,94 @@ def get_active_context_id(*, allowed_providers: set[str] | None = None) -> str |
|
|
|
991
1215
|
|
|
992
1216
|
context_id = (active.context_id or "").strip()
|
|
993
1217
|
return context_id or None
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def collect_tmux_debug_state() -> dict[str, object]:
|
|
1221
|
+
"""Best-effort snapshot of tmux state for diagnosing blank/stuck panes.
|
|
1222
|
+
|
|
1223
|
+
This must be non-intrusive: it should not create sessions or change state.
|
|
1224
|
+
"""
|
|
1225
|
+
|
|
1226
|
+
def _cap(proc: subprocess.CompletedProcess[str] | None) -> dict[str, object]:
|
|
1227
|
+
if proc is None:
|
|
1228
|
+
return {}
|
|
1229
|
+
|
|
1230
|
+
def _trim(s: str | None) -> str:
|
|
1231
|
+
text = (s or "").strip()
|
|
1232
|
+
if len(text) > 4000:
|
|
1233
|
+
return text[:4000] + "…(truncated)"
|
|
1234
|
+
return text
|
|
1235
|
+
|
|
1236
|
+
return {
|
|
1237
|
+
"rc": int(getattr(proc, "returncode", -1)),
|
|
1238
|
+
"stdout": _trim(getattr(proc, "stdout", "")),
|
|
1239
|
+
"stderr": _trim(getattr(proc, "stderr", "")),
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
state: dict[str, object] = {
|
|
1243
|
+
"tmux_available": tmux_available(),
|
|
1244
|
+
"in_tmux": in_tmux(),
|
|
1245
|
+
"managed_env": managed_env_enabled(),
|
|
1246
|
+
"outer_socket": OUTER_SOCKET,
|
|
1247
|
+
"inner_socket": INNER_SOCKET,
|
|
1248
|
+
"outer_session": OUTER_SESSION,
|
|
1249
|
+
"outer_window": OUTER_WINDOW,
|
|
1250
|
+
"inner_session": INNER_SESSION,
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if not tmux_available():
|
|
1254
|
+
return state
|
|
1255
|
+
|
|
1256
|
+
try:
|
|
1257
|
+
state["outer_has_session"] = _cap(
|
|
1258
|
+
_run_outer_tmux(["has-session", "-t", OUTER_SESSION], capture=True, timeout_s=0.5)
|
|
1259
|
+
)
|
|
1260
|
+
state["outer_panes"] = _cap(
|
|
1261
|
+
_run_outer_tmux(
|
|
1262
|
+
[
|
|
1263
|
+
"list-panes",
|
|
1264
|
+
"-t",
|
|
1265
|
+
f"{OUTER_SESSION}:{OUTER_WINDOW}",
|
|
1266
|
+
"-F",
|
|
1267
|
+
"#{pane_index}\t#{pane_active}\t#{pane_current_command}\t#{pane_pid}\t#{pane_tty}",
|
|
1268
|
+
],
|
|
1269
|
+
capture=True,
|
|
1270
|
+
timeout_s=0.5,
|
|
1271
|
+
)
|
|
1272
|
+
)
|
|
1273
|
+
except Exception:
|
|
1274
|
+
pass
|
|
1275
|
+
|
|
1276
|
+
try:
|
|
1277
|
+
state["inner_has_session"] = _cap(
|
|
1278
|
+
_run_inner_tmux(["has-session", "-t", INNER_SESSION], capture=True, timeout_s=0.5)
|
|
1279
|
+
)
|
|
1280
|
+
state["inner_windows"] = _cap(
|
|
1281
|
+
_run_inner_tmux(
|
|
1282
|
+
[
|
|
1283
|
+
"list-windows",
|
|
1284
|
+
"-t",
|
|
1285
|
+
INNER_SESSION,
|
|
1286
|
+
"-F",
|
|
1287
|
+
"#{window_id}\t#{window_index}\t#{window_name}\t#{window_active}\t#{"
|
|
1288
|
+
+ OPT_TERMINAL_ID
|
|
1289
|
+
+ "}\t#{"
|
|
1290
|
+
+ OPT_PROVIDER
|
|
1291
|
+
+ "}\t#{"
|
|
1292
|
+
+ OPT_SESSION_TYPE
|
|
1293
|
+
+ "}\t#{"
|
|
1294
|
+
+ OPT_SESSION_ID
|
|
1295
|
+
+ "}\t#{"
|
|
1296
|
+
+ OPT_CONTEXT_ID
|
|
1297
|
+
+ "}\t#{"
|
|
1298
|
+
+ OPT_ATTENTION
|
|
1299
|
+
+ "}",
|
|
1300
|
+
],
|
|
1301
|
+
capture=True,
|
|
1302
|
+
timeout_s=0.5,
|
|
1303
|
+
)
|
|
1304
|
+
)
|
|
1305
|
+
except Exception:
|
|
1306
|
+
pass
|
|
1307
|
+
|
|
1308
|
+
return state
|