aline-ai 0.6.4__py3-none-any.whl → 0.6.6__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.4.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/RECORD +41 -34
- realign/__init__.py +1 -1
- realign/agent_names.py +79 -0
- realign/claude_hooks/stop_hook.py +3 -0
- realign/claude_hooks/terminal_state.py +11 -0
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +1 -1
- realign/codex_home.py +46 -15
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/doctor.py +3 -1
- realign/commands/export_shares.py +297 -0
- realign/commands/search.py +58 -29
- realign/dashboard/app.py +9 -158
- realign/dashboard/clipboard.py +54 -0
- realign/dashboard/screens/__init__.py +4 -0
- realign/dashboard/screens/agent_detail.py +333 -0
- realign/dashboard/screens/create_agent_info.py +133 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +67 -0
- realign/dashboard/tmux_manager.py +49 -8
- realign/dashboard/widgets/__init__.py +2 -0
- realign/dashboard/widgets/agents_panel.py +1129 -0
- realign/dashboard/widgets/config_panel.py +17 -11
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/dashboard/widgets/terminal_panel.py +109 -31
- realign/db/base.py +27 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +53 -2
- realign/db/sqlite_db.py +185 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +60 -3
- realign/worker_core.py +24 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/top_level.txt +0 -0
|
@@ -393,26 +393,32 @@ class ConfigPanel(Static):
|
|
|
393
393
|
self.app.notify(f"Error saving setting: {e}", title="Config", severity="error")
|
|
394
394
|
|
|
395
395
|
def _handle_doctor(self) -> None:
|
|
396
|
-
"""Run aline doctor
|
|
396
|
+
"""Run aline doctor directly in background thread."""
|
|
397
397
|
self.app.notify("Running Aline Doctor...", title="Doctor")
|
|
398
398
|
|
|
399
399
|
def do_doctor():
|
|
400
400
|
try:
|
|
401
|
-
import
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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:
|
|
409
416
|
self.app.call_from_thread(
|
|
410
417
|
self.app.notify, "Doctor completed successfully", title="Doctor"
|
|
411
418
|
)
|
|
412
419
|
else:
|
|
413
|
-
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
|
414
420
|
self.app.call_from_thread(
|
|
415
|
-
self.app.notify,
|
|
421
|
+
self.app.notify, "Doctor completed with errors", title="Doctor", severity="error"
|
|
416
422
|
)
|
|
417
423
|
except Exception as e:
|
|
418
424
|
self.app.call_from_thread(
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
6
|
import traceback
|
|
10
7
|
from datetime import datetime
|
|
11
8
|
from typing import Optional, Set
|
|
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
|
|
|
20
17
|
from textual.widgets import Button, DataTable, Static
|
|
21
18
|
|
|
22
19
|
from ...logging_config import setup_logger
|
|
20
|
+
from ..clipboard import copy_text
|
|
23
21
|
from .openable_table import OpenableDataTable
|
|
24
22
|
|
|
25
23
|
logger = setup_logger("realign.dashboard.events", "dashboard.log")
|
|
@@ -613,33 +611,12 @@ class EventsTable(Container):
|
|
|
613
611
|
|
|
614
612
|
# Build copy text
|
|
615
613
|
if slack_message:
|
|
616
|
-
|
|
614
|
+
text_to_copy = str(slack_message) + "\n\n" + str(share_link)
|
|
617
615
|
else:
|
|
618
|
-
|
|
616
|
+
text_to_copy = str(share_link)
|
|
619
617
|
|
|
620
618
|
# Copy to clipboard
|
|
621
|
-
copied =
|
|
622
|
-
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
623
|
-
try:
|
|
624
|
-
copied = (
|
|
625
|
-
subprocess.run(
|
|
626
|
-
["pbcopy"],
|
|
627
|
-
input=copy_text,
|
|
628
|
-
text=True,
|
|
629
|
-
capture_output=False,
|
|
630
|
-
check=False,
|
|
631
|
-
).returncode
|
|
632
|
-
== 0
|
|
633
|
-
)
|
|
634
|
-
except Exception:
|
|
635
|
-
copied = False
|
|
636
|
-
|
|
637
|
-
if not copied:
|
|
638
|
-
try:
|
|
639
|
-
self.app.copy_to_clipboard(copy_text)
|
|
640
|
-
copied = True
|
|
641
|
-
except Exception:
|
|
642
|
-
copied = False
|
|
619
|
+
copied = copy_text(self.app, text_to_copy)
|
|
643
620
|
|
|
644
621
|
suffix = " (copied to clipboard)" if copied else ""
|
|
645
622
|
self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
6
|
import traceback
|
|
10
7
|
from datetime import datetime
|
|
11
8
|
from pathlib import Path
|
|
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
|
|
|
20
17
|
from textual.widgets import Button, DataTable, Select, Static
|
|
21
18
|
|
|
22
19
|
from ...logging_config import setup_logger
|
|
20
|
+
from ..clipboard import copy_text
|
|
23
21
|
from .openable_table import OpenableDataTable
|
|
24
22
|
|
|
25
23
|
logger = setup_logger("realign.dashboard.sessions", "dashboard.log")
|
|
@@ -685,33 +683,12 @@ class SessionsTable(Container):
|
|
|
685
683
|
|
|
686
684
|
# Build copy text
|
|
687
685
|
if slack_message:
|
|
688
|
-
|
|
686
|
+
text_to_copy = str(slack_message) + "\n\n" + str(share_link)
|
|
689
687
|
else:
|
|
690
|
-
|
|
688
|
+
text_to_copy = str(share_link)
|
|
691
689
|
|
|
692
690
|
# Copy to clipboard
|
|
693
|
-
copied =
|
|
694
|
-
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
695
|
-
try:
|
|
696
|
-
copied = (
|
|
697
|
-
subprocess.run(
|
|
698
|
-
["pbcopy"],
|
|
699
|
-
input=copy_text,
|
|
700
|
-
text=True,
|
|
701
|
-
capture_output=False,
|
|
702
|
-
check=False,
|
|
703
|
-
).returncode
|
|
704
|
-
== 0
|
|
705
|
-
)
|
|
706
|
-
except Exception:
|
|
707
|
-
copied = False
|
|
708
|
-
|
|
709
|
-
if not copied:
|
|
710
|
-
try:
|
|
711
|
-
self.app.copy_to_clipboard(copy_text)
|
|
712
|
-
copied = True
|
|
713
|
-
except Exception:
|
|
714
|
-
copied = False
|
|
691
|
+
copied = copy_text(self.app, text_to_copy)
|
|
715
692
|
|
|
716
693
|
suffix = " (copied to clipboard)" if copied else ""
|
|
717
694
|
self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
|
|
@@ -456,7 +456,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
456
456
|
try:
|
|
457
457
|
from ...codex_terminal_linker import read_codex_session_meta
|
|
458
458
|
from ...db import get_database
|
|
459
|
-
from ...codex_home import
|
|
459
|
+
from ...codex_home import codex_sessions_dir_for_terminal_or_agent
|
|
460
460
|
except Exception:
|
|
461
461
|
return
|
|
462
462
|
|
|
@@ -474,9 +474,12 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
474
474
|
return
|
|
475
475
|
|
|
476
476
|
candidates: list[Path] = []
|
|
477
|
-
|
|
477
|
+
agent_info_id: str | None = None
|
|
478
|
+
if (agent.source or "").startswith("agent:"):
|
|
479
|
+
agent_info_id = agent.source[6:]
|
|
480
|
+
sessions_root = codex_sessions_dir_for_terminal_or_agent(terminal_id, agent_info_id)
|
|
478
481
|
if sessions_root.exists():
|
|
479
|
-
# Deterministic: isolated per-terminal CODEX_HOME.
|
|
482
|
+
# Deterministic: isolated per-terminal/per-agent CODEX_HOME.
|
|
480
483
|
try:
|
|
481
484
|
candidates = list(sessions_root.rglob("rollout-*.jsonl"))
|
|
482
485
|
except Exception:
|
|
@@ -544,6 +547,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
544
547
|
return
|
|
545
548
|
|
|
546
549
|
try:
|
|
550
|
+
source = "dashboard:auto-link"
|
|
551
|
+
if (agent.source or "").startswith("agent:"):
|
|
552
|
+
source = agent.source or source
|
|
547
553
|
db.update_agent(
|
|
548
554
|
terminal_id,
|
|
549
555
|
provider="codex",
|
|
@@ -552,8 +558,13 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
552
558
|
transcript_path=str(best),
|
|
553
559
|
cwd=cwd,
|
|
554
560
|
project_dir=cwd,
|
|
555
|
-
source=
|
|
561
|
+
source=source,
|
|
556
562
|
)
|
|
563
|
+
if agent_info_id:
|
|
564
|
+
try:
|
|
565
|
+
db.update_session_agent_id(best.stem, agent_info_id)
|
|
566
|
+
except Exception:
|
|
567
|
+
pass
|
|
557
568
|
except Exception:
|
|
558
569
|
return
|
|
559
570
|
|
|
@@ -592,10 +603,16 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
592
603
|
try:
|
|
593
604
|
controls_enabled = self.supported()
|
|
594
605
|
with Horizontal(classes="summary"):
|
|
606
|
+
yield Button(
|
|
607
|
+
"+ New Agent",
|
|
608
|
+
id="quick-new-agent",
|
|
609
|
+
variant="primary",
|
|
610
|
+
disabled=not controls_enabled,
|
|
611
|
+
)
|
|
595
612
|
yield Button(
|
|
596
613
|
"+ Create",
|
|
597
614
|
id="new-agent",
|
|
598
|
-
variant="
|
|
615
|
+
variant="default",
|
|
599
616
|
disabled=not controls_enabled,
|
|
600
617
|
)
|
|
601
618
|
with Vertical(id="terminals", classes="list"):
|
|
@@ -668,13 +685,17 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
668
685
|
|
|
669
686
|
async def refresh_data(self) -> None:
|
|
670
687
|
async with self._refresh_lock:
|
|
688
|
+
t_start = time.time()
|
|
671
689
|
# Check and close stale terminals if enabled
|
|
672
690
|
await self._close_stale_terminals_if_enabled()
|
|
691
|
+
logger.debug(f"[PERF] _close_stale_terminals_if_enabled: {time.time() - t_start:.3f}s")
|
|
673
692
|
|
|
693
|
+
t_refresh = time.time()
|
|
674
694
|
if self._is_native_mode():
|
|
675
695
|
await self._refresh_native_data()
|
|
676
696
|
else:
|
|
677
697
|
await self._refresh_tmux_data()
|
|
698
|
+
logger.debug(f"[PERF] total refresh: {time.time() - t_start:.3f}s")
|
|
678
699
|
|
|
679
700
|
async def _close_stale_terminals_if_enabled(self) -> None:
|
|
680
701
|
"""Close terminals that haven't been updated for the configured hours."""
|
|
@@ -741,15 +762,13 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
741
762
|
logger.error(f"Failed to list native terminals: {e}")
|
|
742
763
|
return
|
|
743
764
|
|
|
744
|
-
#
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
except Exception:
|
|
752
|
-
pass
|
|
765
|
+
# Yield to event loop to keep UI responsive
|
|
766
|
+
await asyncio.sleep(0)
|
|
767
|
+
|
|
768
|
+
# NOTE: _maybe_link_codex_session_for_terminal is intentionally skipped here
|
|
769
|
+
# because it performs expensive file system scans (find_codex_sessions_for_project)
|
|
770
|
+
# that can take minutes with many session files. Codex session linking is handled
|
|
771
|
+
# by the watcher process instead.
|
|
753
772
|
|
|
754
773
|
active_window_id = next(
|
|
755
774
|
(w.session_id for w in windows if w.active), None
|
|
@@ -765,6 +784,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
765
784
|
]
|
|
766
785
|
titles = self._fetch_claude_session_titles(claude_ids)
|
|
767
786
|
|
|
787
|
+
# Yield to event loop after DB query
|
|
788
|
+
await asyncio.sleep(0)
|
|
789
|
+
|
|
768
790
|
# Get context info
|
|
769
791
|
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
770
792
|
all_context_session_ids: set[str] = set()
|
|
@@ -793,6 +815,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
793
815
|
|
|
794
816
|
async def _refresh_tmux_data(self) -> None:
|
|
795
817
|
"""Refresh data using tmux backend."""
|
|
818
|
+
t0 = time.time()
|
|
796
819
|
try:
|
|
797
820
|
supported = self.supported()
|
|
798
821
|
except Exception:
|
|
@@ -806,24 +829,29 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
806
829
|
except Exception:
|
|
807
830
|
return
|
|
808
831
|
windows = [w for w in windows if not self._is_internal_tmux_window(w)]
|
|
832
|
+
logger.debug(f"[PERF] list_inner_windows: {time.time() - t0:.3f}s")
|
|
809
833
|
|
|
810
|
-
#
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
except Exception:
|
|
818
|
-
pass
|
|
834
|
+
# Yield to event loop to keep UI responsive
|
|
835
|
+
await asyncio.sleep(0)
|
|
836
|
+
|
|
837
|
+
# NOTE: _maybe_link_codex_session_for_terminal is intentionally skipped here
|
|
838
|
+
# because it performs expensive file system scans (find_codex_sessions_for_project)
|
|
839
|
+
# that can take minutes with many session files. Codex session linking is handled
|
|
840
|
+
# by the watcher process instead.
|
|
819
841
|
|
|
820
842
|
active_window_id = next((w.window_id for w in windows if w.active), None)
|
|
821
843
|
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
822
844
|
self._expanded_window_id = None
|
|
823
845
|
|
|
846
|
+
t1 = time.time()
|
|
824
847
|
session_ids = [w.session_id for w in windows if self._supports_context(w) and w.session_id]
|
|
825
848
|
titles = self._fetch_claude_session_titles(session_ids)
|
|
849
|
+
logger.debug(f"[PERF] fetch_claude_session_titles: {time.time() - t1:.3f}s")
|
|
850
|
+
|
|
851
|
+
# Yield to event loop after DB query
|
|
852
|
+
await asyncio.sleep(0)
|
|
826
853
|
|
|
854
|
+
t2 = time.time()
|
|
827
855
|
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
828
856
|
all_context_session_ids: set[str] = set()
|
|
829
857
|
for w in windows:
|
|
@@ -840,14 +868,21 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
840
868
|
event_count,
|
|
841
869
|
)
|
|
842
870
|
all_context_session_ids.update(session_ids)
|
|
871
|
+
# Yield periodically during context info gathering
|
|
872
|
+
await asyncio.sleep(0)
|
|
873
|
+
logger.debug(f"[PERF] get_loaded_context_info loop: {time.time() - t2:.3f}s")
|
|
843
874
|
|
|
875
|
+
t3 = time.time()
|
|
844
876
|
if all_context_session_ids:
|
|
845
877
|
titles.update(self._fetch_claude_session_titles(sorted(all_context_session_ids)))
|
|
878
|
+
logger.debug(f"[PERF] fetch context session titles: {time.time() - t3:.3f}s")
|
|
846
879
|
|
|
880
|
+
t4 = time.time()
|
|
847
881
|
try:
|
|
848
882
|
await self._render_terminals_tmux(windows, titles, context_info_by_context_id)
|
|
849
883
|
except Exception:
|
|
850
884
|
return
|
|
885
|
+
logger.debug(f"[PERF] render_terminals_tmux: {time.time() - t4:.3f}s")
|
|
851
886
|
|
|
852
887
|
def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
|
|
853
888
|
# Back-compat hook for tests and older call sites.
|
|
@@ -1226,19 +1261,43 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1226
1261
|
"""Wrap a command to run in a specific directory."""
|
|
1227
1262
|
return f"cd {shlex.quote(directory)} && {command}"
|
|
1228
1263
|
|
|
1264
|
+
async def _quick_create_claude_agent(self) -> None:
|
|
1265
|
+
"""Quickly create a new Claude Code terminal with default settings.
|
|
1266
|
+
|
|
1267
|
+
Uses the last workspace (or cwd) with normal permissions and tracking enabled.
|
|
1268
|
+
"""
|
|
1269
|
+
from ..screens.create_agent import _load_last_workspace
|
|
1270
|
+
|
|
1271
|
+
workspace = _load_last_workspace()
|
|
1272
|
+
self.run_worker(
|
|
1273
|
+
self._create_agent("claude", workspace, skip_permissions=False, no_track=False),
|
|
1274
|
+
group="terminal-panel-create",
|
|
1275
|
+
exclusive=True,
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1229
1278
|
def _on_create_agent_result(self, result: tuple[str, str, bool, bool] | None) -> None:
|
|
1230
1279
|
"""Handle the result from CreateAgentScreen modal."""
|
|
1231
1280
|
if result is None:
|
|
1232
1281
|
return
|
|
1233
1282
|
|
|
1234
1283
|
agent_type, workspace, skip_permissions, no_track = result
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1284
|
+
|
|
1285
|
+
# Capture self reference for use in the deferred callback
|
|
1286
|
+
panel = self
|
|
1287
|
+
|
|
1288
|
+
# Use app.call_later to defer worker creation until after the modal is dismissed.
|
|
1289
|
+
# This ensures the modal screen is fully closed before the worker starts,
|
|
1290
|
+
# preventing UI update conflicts between modal closing and terminal panel refresh.
|
|
1291
|
+
def start_worker() -> None:
|
|
1292
|
+
panel.run_worker(
|
|
1293
|
+
panel._create_agent(
|
|
1294
|
+
agent_type, workspace, skip_permissions=skip_permissions, no_track=no_track
|
|
1295
|
+
),
|
|
1296
|
+
group="terminal-panel-create",
|
|
1297
|
+
exclusive=True,
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
self.app.call_later(start_worker)
|
|
1242
1301
|
|
|
1243
1302
|
async def _create_agent(
|
|
1244
1303
|
self, agent_type: str, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
@@ -1252,7 +1311,14 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1252
1311
|
await self._create_opencode_terminal(workspace)
|
|
1253
1312
|
elif agent_type == "zsh":
|
|
1254
1313
|
await self._create_zsh_terminal(workspace)
|
|
1255
|
-
|
|
1314
|
+
# Schedule refresh in a separate worker to avoid blocking UI.
|
|
1315
|
+
# The refresh involves slow synchronous operations (DB queries, file scans)
|
|
1316
|
+
# that would otherwise freeze the dashboard.
|
|
1317
|
+
self.run_worker(
|
|
1318
|
+
self.refresh_data(),
|
|
1319
|
+
group="terminal-panel-refresh",
|
|
1320
|
+
exclusive=True,
|
|
1321
|
+
)
|
|
1256
1322
|
|
|
1257
1323
|
async def _create_claude_terminal(
|
|
1258
1324
|
self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
@@ -1495,6 +1561,8 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1495
1561
|
|
|
1496
1562
|
async def _create_zsh_terminal(self, workspace: str) -> None:
|
|
1497
1563
|
"""Create a new zsh terminal."""
|
|
1564
|
+
t0 = time.time()
|
|
1565
|
+
logger.info(f"[PERF] _create_zsh_terminal START")
|
|
1498
1566
|
if self._is_native_mode():
|
|
1499
1567
|
backend = await self._ensure_native_backend()
|
|
1500
1568
|
if backend:
|
|
@@ -1509,13 +1577,19 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1509
1577
|
self.app.notify(
|
|
1510
1578
|
"Failed to open zsh terminal", title="Terminal", severity="error"
|
|
1511
1579
|
)
|
|
1580
|
+
logger.info(f"[PERF] _create_zsh_terminal native END: {time.time() - t0:.3f}s")
|
|
1512
1581
|
return
|
|
1513
1582
|
|
|
1514
1583
|
# Tmux fallback
|
|
1584
|
+
t1 = time.time()
|
|
1515
1585
|
command = self._command_in_directory("zsh", workspace)
|
|
1586
|
+
logger.info(f"[PERF] _create_zsh_terminal command ready: {time.time() - t1:.3f}s")
|
|
1587
|
+
t2 = time.time()
|
|
1516
1588
|
created = tmux_manager.create_inner_window("zsh", command)
|
|
1589
|
+
logger.info(f"[PERF] _create_zsh_terminal create_inner_window: {time.time() - t2:.3f}s")
|
|
1517
1590
|
if not created:
|
|
1518
1591
|
self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
|
|
1592
|
+
logger.info(f"[PERF] _create_zsh_terminal TOTAL: {time.time() - t0:.3f}s")
|
|
1519
1593
|
|
|
1520
1594
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
1521
1595
|
button_id = event.button.id or ""
|
|
@@ -1528,6 +1602,10 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1528
1602
|
)
|
|
1529
1603
|
return
|
|
1530
1604
|
|
|
1605
|
+
if button_id == "quick-new-agent":
|
|
1606
|
+
await self._quick_create_claude_agent()
|
|
1607
|
+
return
|
|
1608
|
+
|
|
1531
1609
|
if button_id == "new-agent":
|
|
1532
1610
|
from ..screens import CreateAgentScreen
|
|
1533
1611
|
|
realign/db/base.py
CHANGED
|
@@ -57,6 +57,8 @@ class SessionRecord:
|
|
|
57
57
|
total_turns: Optional[int] = None
|
|
58
58
|
# V12: file mtime when total_turns was cached (for validation)
|
|
59
59
|
total_turns_mtime: Optional[float] = None
|
|
60
|
+
# V19: agent association
|
|
61
|
+
agent_id: Optional[str] = None
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
@dataclass
|
|
@@ -125,6 +127,18 @@ class AgentRecord:
|
|
|
125
127
|
created_by: Optional[str] = None # Creator UID
|
|
126
128
|
|
|
127
129
|
|
|
130
|
+
@dataclass
|
|
131
|
+
class AgentInfoRecord:
|
|
132
|
+
"""Agent profile/identity data (V20)."""
|
|
133
|
+
|
|
134
|
+
id: str
|
|
135
|
+
name: str
|
|
136
|
+
created_at: datetime
|
|
137
|
+
updated_at: datetime
|
|
138
|
+
description: Optional[str] = ""
|
|
139
|
+
visibility: str = "visible"
|
|
140
|
+
|
|
141
|
+
|
|
128
142
|
@dataclass
|
|
129
143
|
class AgentContextRecord:
|
|
130
144
|
"""Represents a context entry (V15: replaces load.json)."""
|
|
@@ -250,6 +264,19 @@ class DatabaseInterface(ABC):
|
|
|
250
264
|
"""
|
|
251
265
|
pass
|
|
252
266
|
|
|
267
|
+
@abstractmethod
|
|
268
|
+
def get_sessions_by_agent_id(self, agent_id: str, limit: int = 1000) -> List[SessionRecord]:
|
|
269
|
+
"""Get all sessions linked to an agent.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
agent_id: The agent_info ID
|
|
273
|
+
limit: Maximum number of sessions to return
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of SessionRecord objects for this agent
|
|
277
|
+
"""
|
|
278
|
+
pass
|
|
279
|
+
|
|
253
280
|
@abstractmethod
|
|
254
281
|
def get_turn_by_hash(self, session_id: str, content_hash: str) -> Optional[TurnRecord]:
|
|
255
282
|
"""Check if a turn with this content hash already exists in the session."""
|
realign/db/locks.py
CHANGED
|
@@ -31,6 +31,10 @@ def lock_key_for_event_summary(event_id: str) -> str:
|
|
|
31
31
|
return f"event_summary:{event_id}"
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def lock_key_for_agent_description(agent_id: str) -> str:
|
|
35
|
+
return f"agent_description:{agent_id}"
|
|
36
|
+
|
|
37
|
+
|
|
34
38
|
@contextmanager
|
|
35
39
|
def lease_lock(
|
|
36
40
|
db: DatabaseInterface,
|
realign/db/schema.py
CHANGED
|
@@ -76,9 +76,15 @@ Schema V18: UID refactor - created_by/shared_by with users table.
|
|
|
76
76
|
- turns: drop uid and user_name (inherit from session)
|
|
77
77
|
- agents: uid -> created_by, drop user_name
|
|
78
78
|
- Update indexes accordingly
|
|
79
|
+
|
|
80
|
+
Schema V19: Agent association for sessions.
|
|
81
|
+
- sessions.agent_id: Logical agent entity association
|
|
82
|
+
|
|
83
|
+
Schema V20: Agent identity/profile table.
|
|
84
|
+
- agent_info table: name, description for agent profiles
|
|
79
85
|
"""
|
|
80
86
|
|
|
81
|
-
SCHEMA_VERSION =
|
|
87
|
+
SCHEMA_VERSION = 21
|
|
82
88
|
|
|
83
89
|
FTS_EVENTS_SCRIPTS = [
|
|
84
90
|
# Full Text Search for Events
|
|
@@ -150,7 +156,8 @@ INIT_SCRIPTS = [
|
|
|
150
156
|
created_by TEXT, -- V18: Creator UID (FK to users.uid)
|
|
151
157
|
shared_by TEXT, -- V18: Sharer UID (who imported this)
|
|
152
158
|
total_turns INTEGER DEFAULT 0, -- V10: Cached total turn count (avoids reading files)
|
|
153
|
-
total_turns_mtime REAL
|
|
159
|
+
total_turns_mtime REAL, -- V12: File mtime when total_turns was cached (for validation)
|
|
160
|
+
agent_id TEXT -- V19: Logical agent association
|
|
154
161
|
);
|
|
155
162
|
""",
|
|
156
163
|
# Turns table (corresponds to git commits, V18: uid/user_name removed)
|
|
@@ -222,6 +229,7 @@ INIT_SCRIPTS = [
|
|
|
222
229
|
"CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity_at DESC);",
|
|
223
230
|
"CREATE INDEX IF NOT EXISTS idx_sessions_type ON sessions(session_type);",
|
|
224
231
|
"CREATE INDEX IF NOT EXISTS idx_sessions_created_by ON sessions(created_by);", # V18
|
|
232
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);", # V19
|
|
225
233
|
"CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);",
|
|
226
234
|
"CREATE INDEX IF NOT EXISTS idx_turns_timestamp ON turns(timestamp DESC);",
|
|
227
235
|
"CREATE INDEX IF NOT EXISTS idx_turns_hash ON turns(content_hash);",
|
|
@@ -335,6 +343,17 @@ INIT_SCRIPTS = [
|
|
|
335
343
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
336
344
|
);
|
|
337
345
|
""",
|
|
346
|
+
# Agent identity/profile table (V20)
|
|
347
|
+
"""
|
|
348
|
+
CREATE TABLE IF NOT EXISTS agent_info (
|
|
349
|
+
id TEXT PRIMARY KEY,
|
|
350
|
+
name TEXT NOT NULL,
|
|
351
|
+
description TEXT DEFAULT '',
|
|
352
|
+
visibility TEXT NOT NULL DEFAULT 'visible',
|
|
353
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
354
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
355
|
+
);
|
|
356
|
+
""",
|
|
338
357
|
*FTS_EVENTS_SCRIPTS,
|
|
339
358
|
]
|
|
340
359
|
|
|
@@ -652,6 +671,29 @@ MIGRATION_V17_TO_V18 = [
|
|
|
652
671
|
]
|
|
653
672
|
|
|
654
673
|
|
|
674
|
+
MIGRATION_V18_TO_V19 = [
|
|
675
|
+
"ALTER TABLE sessions ADD COLUMN agent_id TEXT;",
|
|
676
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);",
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
MIGRATION_V19_TO_V20 = [
|
|
680
|
+
"""
|
|
681
|
+
CREATE TABLE IF NOT EXISTS agent_info (
|
|
682
|
+
id TEXT PRIMARY KEY,
|
|
683
|
+
name TEXT NOT NULL,
|
|
684
|
+
description TEXT DEFAULT '',
|
|
685
|
+
visibility TEXT NOT NULL DEFAULT 'visible',
|
|
686
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
687
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
688
|
+
);
|
|
689
|
+
""",
|
|
690
|
+
]
|
|
691
|
+
|
|
692
|
+
MIGRATION_V20_TO_V21 = [
|
|
693
|
+
"ALTER TABLE agent_info ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible';",
|
|
694
|
+
]
|
|
695
|
+
|
|
696
|
+
|
|
655
697
|
def get_migration_scripts(from_version: int, to_version: int) -> list:
|
|
656
698
|
"""Get migration scripts for upgrading between versions."""
|
|
657
699
|
scripts = []
|
|
@@ -716,4 +758,13 @@ def get_migration_scripts(from_version: int, to_version: int) -> list:
|
|
|
716
758
|
if from_version < 18 and to_version >= 18:
|
|
717
759
|
scripts.extend(MIGRATION_V17_TO_V18)
|
|
718
760
|
|
|
761
|
+
if from_version < 19 and to_version >= 19:
|
|
762
|
+
scripts.extend(MIGRATION_V18_TO_V19)
|
|
763
|
+
|
|
764
|
+
if from_version < 20 and to_version >= 20:
|
|
765
|
+
scripts.extend(MIGRATION_V19_TO_V20)
|
|
766
|
+
|
|
767
|
+
if from_version < 21 and to_version >= 21:
|
|
768
|
+
scripts.extend(MIGRATION_V20_TO_V21)
|
|
769
|
+
|
|
719
770
|
return scripts
|