aline-ai 0.6.3__py3-none-any.whl → 0.6.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.6.3.dist-info → aline_ai-0.6.5.dist-info}/METADATA +1 -1
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.5.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 +497 -0
- realign/commands/init.py +66 -4
- realign/commands/watcher.py +2 -1
- realign/config.py +10 -1
- realign/dashboard/app.py +2 -149
- realign/dashboard/tmux_manager.py +171 -5
- realign/dashboard/widgets/config_panel.py +91 -11
- realign/dashboard/widgets/sessions_table.py +1 -1
- realign/dashboard/widgets/terminal_panel.py +400 -35
- 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.5.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.5.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.5.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.3.dist-info → aline_ai-0.6.5.dist-info}/top_level.txt +0 -0
realign/dashboard/app.py
CHANGED
|
@@ -5,7 +5,6 @@ import subprocess
|
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
7
|
import traceback
|
|
8
|
-
from pathlib import Path
|
|
9
8
|
|
|
10
9
|
from textual.app import App, ComposeResult
|
|
11
10
|
from textual.binding import Binding
|
|
@@ -16,10 +15,7 @@ from .widgets import (
|
|
|
16
15
|
AlineHeader,
|
|
17
16
|
WatcherPanel,
|
|
18
17
|
WorkerPanel,
|
|
19
|
-
SessionsTable,
|
|
20
|
-
EventsTable,
|
|
21
18
|
ConfigPanel,
|
|
22
|
-
SearchPanel,
|
|
23
19
|
TerminalPanel,
|
|
24
20
|
)
|
|
25
21
|
|
|
@@ -71,9 +67,6 @@ class AlineDashboard(App):
|
|
|
71
67
|
Binding("n", "page_next", "Next Page", show=False),
|
|
72
68
|
Binding("p", "page_prev", "Prev Page", show=False),
|
|
73
69
|
Binding("s", "switch_view", "Switch View", show=False),
|
|
74
|
-
Binding("c", "create_event", "Create Event", show=False),
|
|
75
|
-
Binding("l", "load_context", "Load Context", show=False),
|
|
76
|
-
Binding("y", "share_import", "Share Import", show=False),
|
|
77
70
|
Binding("ctrl+c", "quit_confirm", "Quit", priority=True),
|
|
78
71
|
]
|
|
79
72
|
|
|
@@ -119,14 +112,8 @@ class AlineDashboard(App):
|
|
|
119
112
|
yield WatcherPanel()
|
|
120
113
|
with TabPane("Worker", id="worker"):
|
|
121
114
|
yield WorkerPanel()
|
|
122
|
-
with TabPane("Contexts", id="sessions"):
|
|
123
|
-
yield SessionsTable()
|
|
124
|
-
with TabPane("Share", id="events"):
|
|
125
|
-
yield EventsTable()
|
|
126
115
|
with TabPane("Config", id="config"):
|
|
127
116
|
yield ConfigPanel()
|
|
128
|
-
with TabPane("Search", id="search"):
|
|
129
|
-
yield SearchPanel()
|
|
130
117
|
yield Footer()
|
|
131
118
|
logger.debug("compose() completed successfully")
|
|
132
119
|
except Exception as e:
|
|
@@ -135,8 +122,8 @@ class AlineDashboard(App):
|
|
|
135
122
|
|
|
136
123
|
def _tab_ids(self) -> list[str]:
|
|
137
124
|
if self.dev_mode:
|
|
138
|
-
return ["terminal", "watcher", "worker", "
|
|
139
|
-
return ["terminal", "
|
|
125
|
+
return ["terminal", "watcher", "worker", "config"]
|
|
126
|
+
return ["terminal", "config"]
|
|
140
127
|
|
|
141
128
|
def on_mount(self) -> None:
|
|
142
129
|
"""Apply theme based on system settings and watch for changes."""
|
|
@@ -222,14 +209,8 @@ class AlineDashboard(App):
|
|
|
222
209
|
self.query_one(WatcherPanel).refresh_data()
|
|
223
210
|
elif active_tab_id == "worker":
|
|
224
211
|
self.query_one(WorkerPanel).refresh_data()
|
|
225
|
-
elif active_tab_id == "sessions":
|
|
226
|
-
self.query_one(SessionsTable).refresh_data()
|
|
227
|
-
elif active_tab_id == "events":
|
|
228
|
-
self.query_one(EventsTable).refresh_data()
|
|
229
212
|
elif active_tab_id == "config":
|
|
230
213
|
self.query_one(ConfigPanel).refresh_data()
|
|
231
|
-
elif active_tab_id == "search":
|
|
232
|
-
pass # Search is manual
|
|
233
214
|
elif active_tab_id == "terminal":
|
|
234
215
|
await self.query_one(TerminalPanel).refresh_data()
|
|
235
216
|
|
|
@@ -242,7 +223,6 @@ class AlineDashboard(App):
|
|
|
242
223
|
self.query_one(WatcherPanel).action_next_page()
|
|
243
224
|
elif active_tab_id == "worker":
|
|
244
225
|
self.query_one(WorkerPanel).action_next_page()
|
|
245
|
-
# sessions and events tabs use scrolling instead of pagination
|
|
246
226
|
|
|
247
227
|
def action_page_prev(self) -> None:
|
|
248
228
|
"""Go to previous page in current panel."""
|
|
@@ -253,7 +233,6 @@ class AlineDashboard(App):
|
|
|
253
233
|
self.query_one(WatcherPanel).action_prev_page()
|
|
254
234
|
elif active_tab_id == "worker":
|
|
255
235
|
self.query_one(WorkerPanel).action_prev_page()
|
|
256
|
-
# sessions and events tabs use scrolling instead of pagination
|
|
257
236
|
|
|
258
237
|
def action_switch_view(self) -> None:
|
|
259
238
|
"""Switch view in current panel (if supported)."""
|
|
@@ -264,10 +243,6 @@ class AlineDashboard(App):
|
|
|
264
243
|
self.query_one(WatcherPanel).action_switch_view()
|
|
265
244
|
elif active_tab_id == "worker":
|
|
266
245
|
self.query_one(WorkerPanel).action_switch_view()
|
|
267
|
-
elif active_tab_id == "sessions":
|
|
268
|
-
self.query_one(SessionsTable).action_switch_view()
|
|
269
|
-
elif active_tab_id == "events":
|
|
270
|
-
self.query_one(EventsTable).action_switch_view()
|
|
271
246
|
|
|
272
247
|
def action_help(self) -> None:
|
|
273
248
|
"""Show help information."""
|
|
@@ -286,128 +261,6 @@ class AlineDashboard(App):
|
|
|
286
261
|
self._quit_confirm_deadline = now + self._quit_confirm_window_s
|
|
287
262
|
self.notify("Press Ctrl+C again to quit", title="Quit", timeout=2)
|
|
288
263
|
|
|
289
|
-
def action_create_event(self) -> None:
|
|
290
|
-
"""Create an event from selected sessions (Sessions tab only)."""
|
|
291
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
292
|
-
if tabbed_content.active != "sessions":
|
|
293
|
-
self.notify(
|
|
294
|
-
"Switch to the Sessions tab to create an event", title="Create Event", timeout=3
|
|
295
|
-
)
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
sessions_panel = self.query_one(SessionsTable)
|
|
299
|
-
session_ids = sessions_panel.get_selected_session_ids()
|
|
300
|
-
if not session_ids:
|
|
301
|
-
self.notify(
|
|
302
|
-
"No sessions selected (use space / cmd-click / shift-click)",
|
|
303
|
-
title="Create Event",
|
|
304
|
-
timeout=3,
|
|
305
|
-
)
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
from .screens import CreateEventScreen
|
|
309
|
-
|
|
310
|
-
self.push_screen(CreateEventScreen(session_ids))
|
|
311
|
-
|
|
312
|
-
def action_share_import(self) -> None:
|
|
313
|
-
"""Import a share URL (Events tab only)."""
|
|
314
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
315
|
-
if tabbed_content.active != "events":
|
|
316
|
-
self.notify(
|
|
317
|
-
"Switch to the Events tab to import a share", title="Share Import", timeout=3
|
|
318
|
-
)
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
from .screens import ShareImportScreen
|
|
322
|
-
|
|
323
|
-
self.push_screen(ShareImportScreen())
|
|
324
|
-
|
|
325
|
-
async def action_load_context(self) -> None:
|
|
326
|
-
"""Load selected sessions/events into the active Claude terminal context."""
|
|
327
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
328
|
-
active_tab_id = tabbed_content.active
|
|
329
|
-
|
|
330
|
-
try:
|
|
331
|
-
from . import tmux_manager
|
|
332
|
-
|
|
333
|
-
context_id = tmux_manager.get_active_claude_context_id()
|
|
334
|
-
except Exception:
|
|
335
|
-
context_id = None
|
|
336
|
-
|
|
337
|
-
if not context_id:
|
|
338
|
-
self.notify(
|
|
339
|
-
"No active Claude context found. Use the Terminal tab and select a 'cc' terminal (New cc).",
|
|
340
|
-
title="Load Context",
|
|
341
|
-
severity="warning",
|
|
342
|
-
timeout=4,
|
|
343
|
-
)
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
sessions: list[str] = []
|
|
347
|
-
events: list[str] = []
|
|
348
|
-
|
|
349
|
-
if active_tab_id == "sessions":
|
|
350
|
-
sessions = self.query_one(SessionsTable).get_selected_session_ids()
|
|
351
|
-
if not sessions:
|
|
352
|
-
self.notify(
|
|
353
|
-
"No sessions selected (use space / cmd-click / shift-click)",
|
|
354
|
-
title="Load Context",
|
|
355
|
-
severity="warning",
|
|
356
|
-
timeout=3,
|
|
357
|
-
)
|
|
358
|
-
return
|
|
359
|
-
elif active_tab_id == "events":
|
|
360
|
-
events = self.query_one(EventsTable).get_selected_event_ids()
|
|
361
|
-
if not events:
|
|
362
|
-
self.notify(
|
|
363
|
-
"No events selected (use space / cmd-click / shift-click)",
|
|
364
|
-
title="Load Context",
|
|
365
|
-
severity="warning",
|
|
366
|
-
timeout=3,
|
|
367
|
-
)
|
|
368
|
-
return
|
|
369
|
-
else:
|
|
370
|
-
self.notify(
|
|
371
|
-
"Switch to Sessions or Events to load selection into context",
|
|
372
|
-
title="Load Context",
|
|
373
|
-
timeout=3,
|
|
374
|
-
)
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
try:
|
|
378
|
-
from ..context import add_context
|
|
379
|
-
|
|
380
|
-
add_context(
|
|
381
|
-
sessions=sessions or None,
|
|
382
|
-
events=events or None,
|
|
383
|
-
context_id=context_id,
|
|
384
|
-
)
|
|
385
|
-
except Exception as e:
|
|
386
|
-
self.notify(
|
|
387
|
-
f"Failed to load context: {e}",
|
|
388
|
-
title="Load Context",
|
|
389
|
-
severity="error",
|
|
390
|
-
timeout=4,
|
|
391
|
-
)
|
|
392
|
-
return
|
|
393
|
-
|
|
394
|
-
try:
|
|
395
|
-
from .widgets.terminal_panel import TerminalPanel
|
|
396
|
-
|
|
397
|
-
if TerminalPanel.supported():
|
|
398
|
-
await self.query_one(TerminalPanel).refresh_data()
|
|
399
|
-
except Exception:
|
|
400
|
-
pass
|
|
401
|
-
|
|
402
|
-
parts: list[str] = []
|
|
403
|
-
if sessions:
|
|
404
|
-
parts.append(f"{len(sessions)} sessions")
|
|
405
|
-
if events:
|
|
406
|
-
parts.append(f"{len(events)} events")
|
|
407
|
-
what = ", ".join(parts) if parts else "selection"
|
|
408
|
-
self.notify(f"Loaded {what} into {context_id}", title="Load Context", timeout=3)
|
|
409
|
-
|
|
410
|
-
|
|
411
264
|
def run_dashboard(use_native_terminal: bool | None = None) -> None:
|
|
412
265
|
"""Run the Aline Dashboard.
|
|
413
266
|
|
|
@@ -195,11 +195,17 @@ def _session_id_from_transcript_path(transcript_path: str | None) -> str | None:
|
|
|
195
195
|
|
|
196
196
|
def _load_terminal_state_from_db() -> dict[str, dict[str, str]]:
|
|
197
197
|
"""Load terminal state from database (best-effort)."""
|
|
198
|
+
import time as _time
|
|
199
|
+
t0 = _time.time()
|
|
198
200
|
try:
|
|
199
201
|
from ..db import get_database
|
|
200
202
|
|
|
203
|
+
t1 = _time.time()
|
|
201
204
|
db = get_database(read_only=True)
|
|
205
|
+
logger.info(f"[PERF] _load_terminal_state_from_db get_database: {_time.time() - t1:.3f}s")
|
|
206
|
+
t2 = _time.time()
|
|
202
207
|
agents = db.list_agents(status="active", limit=100)
|
|
208
|
+
logger.info(f"[PERF] _load_terminal_state_from_db list_agents: {_time.time() - t2:.3f}s")
|
|
203
209
|
|
|
204
210
|
out: dict[str, dict[str, str]] = {}
|
|
205
211
|
for agent in agents:
|
|
@@ -510,14 +516,43 @@ def bootstrap_dashboard_into_tmux() -> None:
|
|
|
510
516
|
os.execvp("tmux", ["tmux", "-L", OUTER_SOCKET, "attach", "-t", OUTER_SESSION])
|
|
511
517
|
|
|
512
518
|
|
|
519
|
+
_inner_session_configured = False
|
|
520
|
+
|
|
521
|
+
|
|
513
522
|
def ensure_inner_session() -> bool:
|
|
514
|
-
"""Ensure the inner tmux server/session exists (returns True on success).
|
|
523
|
+
"""Ensure the inner tmux server/session exists (returns True on success).
|
|
524
|
+
|
|
525
|
+
The full configuration (mouse, status bar, border styles, home window setup) is
|
|
526
|
+
only applied once per process lifetime. Subsequent calls just verify the session
|
|
527
|
+
is still alive via a cheap ``has-session`` check.
|
|
528
|
+
"""
|
|
529
|
+
global _inner_session_configured
|
|
530
|
+
|
|
515
531
|
if not (tmux_available() and in_tmux() and managed_env_enabled()):
|
|
516
532
|
return False
|
|
517
533
|
|
|
518
534
|
if _run_inner_tmux(["has-session", "-t", INNER_SESSION]).returncode != 0:
|
|
519
|
-
|
|
535
|
+
# Create a stable "home" window so user-created terminals can use names like "zsh"
|
|
536
|
+
# without always becoming "zsh-2".
|
|
537
|
+
if (
|
|
538
|
+
_run_inner_tmux(["new-session", "-d", "-s", INNER_SESSION, "-n", "home"]).returncode
|
|
539
|
+
!= 0
|
|
540
|
+
):
|
|
520
541
|
return False
|
|
542
|
+
# Force re-configuration after creating a new session.
|
|
543
|
+
_inner_session_configured = False
|
|
544
|
+
|
|
545
|
+
if _inner_session_configured:
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
# --- One-time configuration below ---
|
|
549
|
+
|
|
550
|
+
# Ensure the default/home window stays named "home" (tmux auto-rename would otherwise
|
|
551
|
+
# change it to "zsh"/"opencode" depending on the last foreground command).
|
|
552
|
+
try:
|
|
553
|
+
_ensure_inner_home_window()
|
|
554
|
+
except Exception:
|
|
555
|
+
pass
|
|
521
556
|
|
|
522
557
|
# Dedicated inner server; safe to enable mouse globally there.
|
|
523
558
|
_run_inner_tmux(["set-option", "-g", "mouse", "on"])
|
|
@@ -534,9 +569,104 @@ def ensure_inner_session() -> bool:
|
|
|
534
569
|
_run_inner_tmux(["set-option", "-g", "pane-border-indicators", "arrows"])
|
|
535
570
|
|
|
536
571
|
_source_aline_tmux_config(_run_inner_tmux)
|
|
572
|
+
|
|
573
|
+
_inner_session_configured = True
|
|
537
574
|
return True
|
|
538
575
|
|
|
539
576
|
|
|
577
|
+
def _ensure_inner_home_window() -> None:
|
|
578
|
+
"""Ensure the inner session has a reserved, non-renaming 'home' window (best-effort)."""
|
|
579
|
+
if _run_inner_tmux(["has-session", "-t", INNER_SESSION]).returncode != 0:
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
out = (
|
|
583
|
+
_run_inner_tmux(
|
|
584
|
+
[
|
|
585
|
+
"list-windows",
|
|
586
|
+
"-t",
|
|
587
|
+
INNER_SESSION,
|
|
588
|
+
"-F",
|
|
589
|
+
"#{window_id}\t#{window_index}\t#{window_name}\t#{"
|
|
590
|
+
+ OPT_TERMINAL_ID
|
|
591
|
+
+ "}\t#{"
|
|
592
|
+
+ OPT_PROVIDER
|
|
593
|
+
+ "}\t#{"
|
|
594
|
+
+ OPT_SESSION_TYPE
|
|
595
|
+
+ "}\t#{"
|
|
596
|
+
+ OPT_CONTEXT_ID
|
|
597
|
+
+ "}\t#{"
|
|
598
|
+
+ OPT_CREATED_AT
|
|
599
|
+
+ "}\t#{"
|
|
600
|
+
+ OPT_NO_TRACK
|
|
601
|
+
+ "}",
|
|
602
|
+
],
|
|
603
|
+
capture=True,
|
|
604
|
+
).stdout
|
|
605
|
+
or ""
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
candidates: list[tuple[str, int, str, str, str, str, str, str, str]] = []
|
|
609
|
+
for line in _parse_lines(out):
|
|
610
|
+
parts = (line.split("\t", 8) + [""] * 9)[:9]
|
|
611
|
+
window_id = parts[0]
|
|
612
|
+
try:
|
|
613
|
+
window_index = int(parts[1])
|
|
614
|
+
except Exception:
|
|
615
|
+
window_index = 9999
|
|
616
|
+
window_name = parts[2]
|
|
617
|
+
terminal_id = parts[3]
|
|
618
|
+
provider = parts[4]
|
|
619
|
+
session_type = parts[5]
|
|
620
|
+
context_id = parts[6]
|
|
621
|
+
created_at = parts[7]
|
|
622
|
+
no_track = parts[8]
|
|
623
|
+
|
|
624
|
+
# Pick an unmanaged window (the default one created by `new-session`) as "home".
|
|
625
|
+
unmanaged = (
|
|
626
|
+
not (terminal_id or "").strip()
|
|
627
|
+
and not (provider or "").strip()
|
|
628
|
+
and not (session_type or "").strip()
|
|
629
|
+
and not (context_id or "").strip()
|
|
630
|
+
and not (created_at or "").strip()
|
|
631
|
+
)
|
|
632
|
+
if unmanaged:
|
|
633
|
+
candidates.append(
|
|
634
|
+
(
|
|
635
|
+
window_id,
|
|
636
|
+
window_index,
|
|
637
|
+
window_name,
|
|
638
|
+
terminal_id,
|
|
639
|
+
provider,
|
|
640
|
+
session_type,
|
|
641
|
+
context_id,
|
|
642
|
+
created_at,
|
|
643
|
+
no_track,
|
|
644
|
+
)
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
if not candidates:
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
# Prefer the first window (index 0) if present.
|
|
651
|
+
candidates.sort(key=lambda t: t[1])
|
|
652
|
+
window_id = candidates[0][0]
|
|
653
|
+
|
|
654
|
+
# Rename to "home" and prevent tmux auto-renaming it based on foreground command.
|
|
655
|
+
_run_inner_tmux(["rename-window", "-t", window_id, "home"])
|
|
656
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, "automatic-rename", "off"])
|
|
657
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, "allow-rename", "off"])
|
|
658
|
+
|
|
659
|
+
# Mark as internal/no-track so UI can hide it.
|
|
660
|
+
# NOTE: We use _run_inner_tmux directly here instead of set_inner_window_options
|
|
661
|
+
# to avoid recursion: set_inner_window_options → ensure_inner_session →
|
|
662
|
+
# _ensure_inner_home_window → set_inner_window_options.
|
|
663
|
+
try:
|
|
664
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, OPT_NO_TRACK, "1"])
|
|
665
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, OPT_CREATED_AT, str(time.time())])
|
|
666
|
+
except Exception:
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
|
|
540
670
|
def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
541
671
|
"""Create the right-side pane (terminal area) if it doesn't exist.
|
|
542
672
|
|
|
@@ -580,9 +710,14 @@ def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
|
580
710
|
|
|
581
711
|
|
|
582
712
|
def list_inner_windows() -> list[InnerWindow]:
|
|
713
|
+
import time as _time
|
|
714
|
+
t0 = _time.time()
|
|
583
715
|
if not ensure_inner_session():
|
|
584
716
|
return []
|
|
717
|
+
logger.info(f"[PERF] list_inner_windows ensure_inner_session: {_time.time() - t0:.3f}s")
|
|
718
|
+
t1 = _time.time()
|
|
585
719
|
state = _load_terminal_state()
|
|
720
|
+
logger.info(f"[PERF] list_inner_windows _load_terminal_state: {_time.time() - t1:.3f}s")
|
|
586
721
|
out = (
|
|
587
722
|
_run_inner_tmux(
|
|
588
723
|
[
|
|
@@ -678,13 +813,16 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
678
813
|
|
|
679
814
|
|
|
680
815
|
def set_inner_window_options(window_id: str, options: dict[str, str]) -> bool:
|
|
816
|
+
import time as _time
|
|
681
817
|
if not ensure_inner_session():
|
|
682
818
|
return False
|
|
683
819
|
ok = True
|
|
684
820
|
for key, value in options.items():
|
|
821
|
+
t0 = _time.time()
|
|
685
822
|
# Important: these are per-window (not session-wide) to avoid cross-tab clobbering.
|
|
686
823
|
if _run_inner_tmux(["set-option", "-w", "-t", window_id, key, value]).returncode != 0:
|
|
687
824
|
ok = False
|
|
825
|
+
logger.info(f"[PERF] set_inner_window_options {key}: {_time.time() - t0:.3f}s")
|
|
688
826
|
return ok
|
|
689
827
|
|
|
690
828
|
|
|
@@ -701,16 +839,24 @@ def create_inner_window(
|
|
|
701
839
|
terminal_id: str | None = None,
|
|
702
840
|
provider: str | None = None,
|
|
703
841
|
context_id: str | None = None,
|
|
842
|
+
no_track: bool = False,
|
|
704
843
|
) -> InnerWindow | None:
|
|
844
|
+
import time as _time
|
|
845
|
+
t0 = _time.time()
|
|
846
|
+
logger.info(f"[PERF] create_inner_window START")
|
|
705
847
|
if not ensure_right_pane():
|
|
706
848
|
return None
|
|
849
|
+
logger.info(f"[PERF] create_inner_window ensure_right_pane: {_time.time() - t0:.3f}s")
|
|
707
850
|
|
|
851
|
+
t1 = _time.time()
|
|
708
852
|
existing = list_inner_windows()
|
|
853
|
+
logger.info(f"[PERF] create_inner_window list_inner_windows: {_time.time() - t1:.3f}s")
|
|
709
854
|
name = _unique_name((w.window_name for w in existing), base_name)
|
|
710
855
|
|
|
711
856
|
# Record creation time before creating the window
|
|
712
857
|
created_at = time.time()
|
|
713
858
|
|
|
859
|
+
t2 = _time.time()
|
|
714
860
|
proc = _run_inner_tmux(
|
|
715
861
|
[
|
|
716
862
|
"new-window",
|
|
@@ -725,6 +871,7 @@ def create_inner_window(
|
|
|
725
871
|
],
|
|
726
872
|
capture=True,
|
|
727
873
|
)
|
|
874
|
+
logger.info(f"[PERF] create_inner_window new-window: {_time.time() - t2:.3f}s")
|
|
728
875
|
if proc.returncode != 0:
|
|
729
876
|
return None
|
|
730
877
|
|
|
@@ -744,7 +891,13 @@ def create_inner_window(
|
|
|
744
891
|
opts.setdefault(OPT_SESSION_TYPE, "")
|
|
745
892
|
opts.setdefault(OPT_SESSION_ID, "")
|
|
746
893
|
opts.setdefault(OPT_TRANSCRIPT_PATH, "")
|
|
894
|
+
if no_track:
|
|
895
|
+
opts[OPT_NO_TRACK] = "1"
|
|
896
|
+
else:
|
|
897
|
+
opts.setdefault(OPT_NO_TRACK, "")
|
|
898
|
+
t3 = _time.time()
|
|
747
899
|
set_inner_window_options(window_id, opts)
|
|
900
|
+
logger.info(f"[PERF] create_inner_window set_options: {_time.time() - t3:.3f}s")
|
|
748
901
|
|
|
749
902
|
_run_inner_tmux(["select-window", "-t", window_id])
|
|
750
903
|
|
|
@@ -784,6 +937,16 @@ def clear_attention(window_id: str) -> bool:
|
|
|
784
937
|
|
|
785
938
|
def get_active_claude_context_id() -> str | None:
|
|
786
939
|
"""Return the active inner tmux window's Claude ALINE_CONTEXT_ID (if any)."""
|
|
940
|
+
return get_active_context_id(allowed_providers={"claude"})
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def get_active_codex_context_id() -> str | None:
|
|
944
|
+
"""Return the active inner tmux window's Codex ALINE_CONTEXT_ID (if any)."""
|
|
945
|
+
return get_active_context_id(allowed_providers={"codex"})
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def get_active_context_id(*, allowed_providers: set[str] | None = None) -> str | None:
|
|
949
|
+
"""Return the active inner tmux window's ALINE_CONTEXT_ID (optionally filtered by provider)."""
|
|
787
950
|
try:
|
|
788
951
|
windows = list_inner_windows()
|
|
789
952
|
except Exception:
|
|
@@ -793,9 +956,12 @@ def get_active_claude_context_id() -> str | None:
|
|
|
793
956
|
if active is None:
|
|
794
957
|
return None
|
|
795
958
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
959
|
+
if allowed_providers is not None:
|
|
960
|
+
allowed = {str(p).strip() for p in allowed_providers if str(p).strip()}
|
|
961
|
+
provider = (active.provider or "").strip()
|
|
962
|
+
session_type = (active.session_type or "").strip()
|
|
963
|
+
if provider not in allowed and session_type not in allowed:
|
|
964
|
+
return None
|
|
799
965
|
|
|
800
966
|
context_id = (active.context_id or "").strip()
|
|
801
967
|
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,27 +358,67 @@ 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
|
-
"""Run aline doctor
|
|
396
|
+
"""Run aline doctor directly in background thread."""
|
|
323
397
|
self.app.notify("Running Aline Doctor...", title="Doctor")
|
|
324
398
|
|
|
325
399
|
def do_doctor():
|
|
326
400
|
try:
|
|
327
|
-
import
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
401
|
+
import contextlib
|
|
402
|
+
import io
|
|
403
|
+
from ...commands.doctor import run_doctor
|
|
404
|
+
|
|
405
|
+
# Suppress Rich console output (would corrupt TUI)
|
|
406
|
+
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
407
|
+
exit_code = run_doctor(
|
|
408
|
+
restart_daemons=True,
|
|
409
|
+
start_if_not_running=False,
|
|
410
|
+
verbose=False,
|
|
411
|
+
clear_cache=True,
|
|
412
|
+
auto_fix=True,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if exit_code == 0:
|
|
335
416
|
self.app.call_from_thread(
|
|
336
417
|
self.app.notify, "Doctor completed successfully", title="Doctor"
|
|
337
418
|
)
|
|
338
419
|
else:
|
|
339
|
-
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
|
340
420
|
self.app.call_from_thread(
|
|
341
|
-
self.app.notify,
|
|
421
|
+
self.app.notify, "Doctor completed with errors", title="Doctor", severity="error"
|
|
342
422
|
)
|
|
343
423
|
except Exception as e:
|
|
344
424
|
self.app.call_from_thread(
|
|
@@ -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
|