aline-ai 0.6.3__py3-none-any.whl → 0.6.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.6.3.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.4.dist-info}/RECORD +26 -23
- realign/__init__.py +1 -1
- realign/adapters/codex.py +14 -9
- realign/cli.py +42 -235
- realign/codex_detector.py +72 -32
- realign/codex_home.py +85 -0
- realign/codex_terminal_linker.py +172 -0
- realign/commands/__init__.py +2 -2
- realign/commands/add.py +89 -9
- realign/commands/doctor.py +495 -0
- realign/commands/init.py +66 -4
- realign/commands/watcher.py +2 -1
- realign/config.py +10 -1
- realign/dashboard/app.py +5 -3
- realign/dashboard/tmux_manager.py +129 -4
- realign/dashboard/widgets/config_panel.py +74 -0
- realign/dashboard/widgets/sessions_table.py +1 -1
- realign/dashboard/widgets/terminal_panel.py +349 -27
- realign/db/sqlite_db.py +76 -0
- realign/hooks.py +6 -128
- realign/watcher_core.py +50 -0
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.4.dist-info}/top_level.txt +0 -0
|
@@ -516,9 +516,21 @@ def ensure_inner_session() -> bool:
|
|
|
516
516
|
return False
|
|
517
517
|
|
|
518
518
|
if _run_inner_tmux(["has-session", "-t", INNER_SESSION]).returncode != 0:
|
|
519
|
-
|
|
519
|
+
# Create a stable "home" window so user-created terminals can use names like "zsh"
|
|
520
|
+
# without always becoming "zsh-2".
|
|
521
|
+
if (
|
|
522
|
+
_run_inner_tmux(["new-session", "-d", "-s", INNER_SESSION, "-n", "home"]).returncode
|
|
523
|
+
!= 0
|
|
524
|
+
):
|
|
520
525
|
return False
|
|
521
526
|
|
|
527
|
+
# Ensure the default/home window stays named "home" (tmux auto-rename would otherwise
|
|
528
|
+
# change it to "zsh"/"opencode" depending on the last foreground command).
|
|
529
|
+
try:
|
|
530
|
+
_ensure_inner_home_window()
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
|
|
522
534
|
# Dedicated inner server; safe to enable mouse globally there.
|
|
523
535
|
_run_inner_tmux(["set-option", "-g", "mouse", "on"])
|
|
524
536
|
|
|
@@ -537,6 +549,101 @@ def ensure_inner_session() -> bool:
|
|
|
537
549
|
return True
|
|
538
550
|
|
|
539
551
|
|
|
552
|
+
def _ensure_inner_home_window() -> None:
|
|
553
|
+
"""Ensure the inner session has a reserved, non-renaming 'home' window (best-effort)."""
|
|
554
|
+
if _run_inner_tmux(["has-session", "-t", INNER_SESSION]).returncode != 0:
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
out = (
|
|
558
|
+
_run_inner_tmux(
|
|
559
|
+
[
|
|
560
|
+
"list-windows",
|
|
561
|
+
"-t",
|
|
562
|
+
INNER_SESSION,
|
|
563
|
+
"-F",
|
|
564
|
+
"#{window_id}\t#{window_index}\t#{window_name}\t#{"
|
|
565
|
+
+ OPT_TERMINAL_ID
|
|
566
|
+
+ "}\t#{"
|
|
567
|
+
+ OPT_PROVIDER
|
|
568
|
+
+ "}\t#{"
|
|
569
|
+
+ OPT_SESSION_TYPE
|
|
570
|
+
+ "}\t#{"
|
|
571
|
+
+ OPT_CONTEXT_ID
|
|
572
|
+
+ "}\t#{"
|
|
573
|
+
+ OPT_CREATED_AT
|
|
574
|
+
+ "}\t#{"
|
|
575
|
+
+ OPT_NO_TRACK
|
|
576
|
+
+ "}",
|
|
577
|
+
],
|
|
578
|
+
capture=True,
|
|
579
|
+
).stdout
|
|
580
|
+
or ""
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
candidates: list[tuple[str, int, str, str, str, str, str, str, str]] = []
|
|
584
|
+
for line in _parse_lines(out):
|
|
585
|
+
parts = (line.split("\t", 8) + [""] * 9)[:9]
|
|
586
|
+
window_id = parts[0]
|
|
587
|
+
try:
|
|
588
|
+
window_index = int(parts[1])
|
|
589
|
+
except Exception:
|
|
590
|
+
window_index = 9999
|
|
591
|
+
window_name = parts[2]
|
|
592
|
+
terminal_id = parts[3]
|
|
593
|
+
provider = parts[4]
|
|
594
|
+
session_type = parts[5]
|
|
595
|
+
context_id = parts[6]
|
|
596
|
+
created_at = parts[7]
|
|
597
|
+
no_track = parts[8]
|
|
598
|
+
|
|
599
|
+
# Pick an unmanaged window (the default one created by `new-session`) as "home".
|
|
600
|
+
unmanaged = (
|
|
601
|
+
not (terminal_id or "").strip()
|
|
602
|
+
and not (provider or "").strip()
|
|
603
|
+
and not (session_type or "").strip()
|
|
604
|
+
and not (context_id or "").strip()
|
|
605
|
+
and not (created_at or "").strip()
|
|
606
|
+
)
|
|
607
|
+
if unmanaged:
|
|
608
|
+
candidates.append(
|
|
609
|
+
(
|
|
610
|
+
window_id,
|
|
611
|
+
window_index,
|
|
612
|
+
window_name,
|
|
613
|
+
terminal_id,
|
|
614
|
+
provider,
|
|
615
|
+
session_type,
|
|
616
|
+
context_id,
|
|
617
|
+
created_at,
|
|
618
|
+
no_track,
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
if not candidates:
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
# Prefer the first window (index 0) if present.
|
|
626
|
+
candidates.sort(key=lambda t: t[1])
|
|
627
|
+
window_id = candidates[0][0]
|
|
628
|
+
|
|
629
|
+
# Rename to "home" and prevent tmux auto-renaming it based on foreground command.
|
|
630
|
+
_run_inner_tmux(["rename-window", "-t", window_id, "home"])
|
|
631
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, "automatic-rename", "off"])
|
|
632
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, "allow-rename", "off"])
|
|
633
|
+
|
|
634
|
+
# Mark as internal/no-track so UI can hide it.
|
|
635
|
+
try:
|
|
636
|
+
set_inner_window_options(
|
|
637
|
+
window_id,
|
|
638
|
+
{
|
|
639
|
+
OPT_NO_TRACK: "1",
|
|
640
|
+
OPT_CREATED_AT: str(time.time()),
|
|
641
|
+
},
|
|
642
|
+
)
|
|
643
|
+
except Exception:
|
|
644
|
+
pass
|
|
645
|
+
|
|
646
|
+
|
|
540
647
|
def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
541
648
|
"""Create the right-side pane (terminal area) if it doesn't exist.
|
|
542
649
|
|
|
@@ -701,6 +808,7 @@ def create_inner_window(
|
|
|
701
808
|
terminal_id: str | None = None,
|
|
702
809
|
provider: str | None = None,
|
|
703
810
|
context_id: str | None = None,
|
|
811
|
+
no_track: bool = False,
|
|
704
812
|
) -> InnerWindow | None:
|
|
705
813
|
if not ensure_right_pane():
|
|
706
814
|
return None
|
|
@@ -744,6 +852,10 @@ def create_inner_window(
|
|
|
744
852
|
opts.setdefault(OPT_SESSION_TYPE, "")
|
|
745
853
|
opts.setdefault(OPT_SESSION_ID, "")
|
|
746
854
|
opts.setdefault(OPT_TRANSCRIPT_PATH, "")
|
|
855
|
+
if no_track:
|
|
856
|
+
opts[OPT_NO_TRACK] = "1"
|
|
857
|
+
else:
|
|
858
|
+
opts.setdefault(OPT_NO_TRACK, "")
|
|
747
859
|
set_inner_window_options(window_id, opts)
|
|
748
860
|
|
|
749
861
|
_run_inner_tmux(["select-window", "-t", window_id])
|
|
@@ -784,6 +896,16 @@ def clear_attention(window_id: str) -> bool:
|
|
|
784
896
|
|
|
785
897
|
def get_active_claude_context_id() -> str | None:
|
|
786
898
|
"""Return the active inner tmux window's Claude ALINE_CONTEXT_ID (if any)."""
|
|
899
|
+
return get_active_context_id(allowed_providers={"claude"})
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def get_active_codex_context_id() -> str | None:
|
|
903
|
+
"""Return the active inner tmux window's Codex ALINE_CONTEXT_ID (if any)."""
|
|
904
|
+
return get_active_context_id(allowed_providers={"codex"})
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def get_active_context_id(*, allowed_providers: set[str] | None = None) -> str | None:
|
|
908
|
+
"""Return the active inner tmux window's ALINE_CONTEXT_ID (optionally filtered by provider)."""
|
|
787
909
|
try:
|
|
788
910
|
windows = list_inner_windows()
|
|
789
911
|
except Exception:
|
|
@@ -793,9 +915,12 @@ def get_active_claude_context_id() -> str | None:
|
|
|
793
915
|
if active is None:
|
|
794
916
|
return None
|
|
795
917
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
918
|
+
if allowed_providers is not None:
|
|
919
|
+
allowed = {str(p).strip() for p in allowed_providers if str(p).strip()}
|
|
920
|
+
provider = (active.provider or "").strip()
|
|
921
|
+
session_type = (active.session_type or "").strip()
|
|
922
|
+
if provider not in allowed and session_type not in allowed:
|
|
923
|
+
return None
|
|
799
924
|
|
|
800
925
|
context_id = (active.context_id or "").strip()
|
|
801
926
|
return context_id or None
|
|
@@ -89,6 +89,30 @@ class ConfigPanel(Static):
|
|
|
89
89
|
height: auto;
|
|
90
90
|
margin-top: 2;
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
ConfigPanel .terminal-settings {
|
|
94
|
+
height: auto;
|
|
95
|
+
margin-top: 2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ConfigPanel .terminal-settings .setting-row {
|
|
99
|
+
height: auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ConfigPanel .terminal-settings .setting-label {
|
|
103
|
+
width: auto;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ConfigPanel .terminal-settings RadioSet {
|
|
107
|
+
width: auto;
|
|
108
|
+
height: auto;
|
|
109
|
+
layout: horizontal;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ConfigPanel .terminal-settings RadioButton {
|
|
113
|
+
width: auto;
|
|
114
|
+
margin-right: 2;
|
|
115
|
+
}
|
|
92
116
|
"""
|
|
93
117
|
|
|
94
118
|
def __init__(self) -> None:
|
|
@@ -97,6 +121,7 @@ class ConfigPanel(Static):
|
|
|
97
121
|
self._syncing_radio: bool = False # Flag to prevent recursive radio updates
|
|
98
122
|
self._login_in_progress: bool = False # Track login state
|
|
99
123
|
self._refresh_timer = None # Timer for auto-refresh
|
|
124
|
+
self._auto_close_stale_enabled: bool = False # Track auto-close setting
|
|
100
125
|
|
|
101
126
|
def compose(self) -> ComposeResult:
|
|
102
127
|
"""Compose the config panel layout."""
|
|
@@ -115,6 +140,15 @@ class ConfigPanel(Static):
|
|
|
115
140
|
yield RadioButton("Enabled", id="border-resize-enabled", value=True)
|
|
116
141
|
yield RadioButton("Disabled", id="border-resize-disabled")
|
|
117
142
|
|
|
143
|
+
# Terminal settings section
|
|
144
|
+
with Static(classes="terminal-settings"):
|
|
145
|
+
yield Static("[bold]Terminal Settings[/bold]", classes="section-title")
|
|
146
|
+
with Horizontal(classes="setting-row"):
|
|
147
|
+
yield Static("Auto-close stale terminals (24h):", classes="setting-label")
|
|
148
|
+
with RadioSet(id="auto-close-stale-radio"):
|
|
149
|
+
yield RadioButton("Enabled", id="auto-close-stale-enabled")
|
|
150
|
+
yield RadioButton("Disabled", id="auto-close-stale-disabled", value=True)
|
|
151
|
+
|
|
118
152
|
# Tools section
|
|
119
153
|
with Static(classes="tools-section"):
|
|
120
154
|
yield Static("[bold]Tools[/bold]", classes="section-title")
|
|
@@ -129,6 +163,9 @@ class ConfigPanel(Static):
|
|
|
129
163
|
# Query and set the actual tmux border resize state
|
|
130
164
|
self._sync_border_resize_radio()
|
|
131
165
|
|
|
166
|
+
# Sync auto-close stale terminals setting from config
|
|
167
|
+
self._sync_auto_close_stale_radio()
|
|
168
|
+
|
|
132
169
|
# Start timer to periodically refresh account status (every 5 seconds)
|
|
133
170
|
self._refresh_timer = self.set_interval(5.0, self._update_account_status)
|
|
134
171
|
|
|
@@ -151,6 +188,9 @@ class ConfigPanel(Static):
|
|
|
151
188
|
# Check which radio button is selected
|
|
152
189
|
enabled = event.pressed.id == "border-resize-enabled"
|
|
153
190
|
self._toggle_border_resize(enabled)
|
|
191
|
+
elif event.radio_set.id == "auto-close-stale-radio":
|
|
192
|
+
enabled = event.pressed.id == "auto-close-stale-enabled"
|
|
193
|
+
self._toggle_auto_close_stale(enabled)
|
|
154
194
|
|
|
155
195
|
def _update_account_status(self) -> None:
|
|
156
196
|
"""Update the account status display."""
|
|
@@ -318,6 +358,40 @@ class ConfigPanel(Static):
|
|
|
318
358
|
except Exception as e:
|
|
319
359
|
self.app.notify(f"Error toggling border resize: {e}", title="Tmux", severity="error")
|
|
320
360
|
|
|
361
|
+
def _sync_auto_close_stale_radio(self) -> None:
|
|
362
|
+
"""Sync radio buttons with config file setting."""
|
|
363
|
+
try:
|
|
364
|
+
config = ReAlignConfig.load()
|
|
365
|
+
is_enabled = config.auto_close_stale_terminals
|
|
366
|
+
self._auto_close_stale_enabled = is_enabled
|
|
367
|
+
|
|
368
|
+
# Update radio buttons without triggering the toggle action
|
|
369
|
+
self._syncing_radio = True
|
|
370
|
+
try:
|
|
371
|
+
if is_enabled:
|
|
372
|
+
radio = self.query_one("#auto-close-stale-enabled", RadioButton)
|
|
373
|
+
else:
|
|
374
|
+
radio = self.query_one("#auto-close-stale-disabled", RadioButton)
|
|
375
|
+
radio.value = True
|
|
376
|
+
finally:
|
|
377
|
+
self._syncing_radio = False
|
|
378
|
+
except Exception:
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
def _toggle_auto_close_stale(self, enabled: bool) -> None:
|
|
382
|
+
"""Enable or disable auto-close stale terminals setting."""
|
|
383
|
+
try:
|
|
384
|
+
config = ReAlignConfig.load()
|
|
385
|
+
config.auto_close_stale_terminals = enabled
|
|
386
|
+
config.save()
|
|
387
|
+
self._auto_close_stale_enabled = enabled
|
|
388
|
+
if enabled:
|
|
389
|
+
self.app.notify("Auto-close stale terminals enabled", title="Terminal")
|
|
390
|
+
else:
|
|
391
|
+
self.app.notify("Auto-close stale terminals disabled", title="Terminal")
|
|
392
|
+
except Exception as e:
|
|
393
|
+
self.app.notify(f"Error saving setting: {e}", title="Config", severity="error")
|
|
394
|
+
|
|
321
395
|
def _handle_doctor(self) -> None:
|
|
322
396
|
"""Run aline doctor command in background."""
|
|
323
397
|
self.app.notify("Running Aline Doctor...", title="Doctor")
|
|
@@ -17,7 +17,7 @@ from textual.binding import Binding
|
|
|
17
17
|
from textual.containers import Container, Horizontal, Vertical
|
|
18
18
|
from textual.reactive import reactive
|
|
19
19
|
from textual.worker import Worker, WorkerState
|
|
20
|
-
from textual.widgets import Button, DataTable, Static
|
|
20
|
+
from textual.widgets import Button, DataTable, Select, Static
|
|
21
21
|
|
|
22
22
|
from ...logging_config import setup_logger
|
|
23
23
|
from .openable_table import OpenableDataTable
|