aline-ai 0.5.13__py3-none-any.whl → 0.6.1__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.5.13.dist-info → aline_ai-0.6.1.dist-info}/METADATA +1 -1
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/RECORD +24 -19
- realign/__init__.py +1 -1
- realign/auth.py +21 -0
- realign/cli.py +293 -6
- realign/commands/auth.py +110 -0
- realign/commands/import_shares.py +6 -0
- realign/commands/watcher.py +8 -0
- realign/commands/worker.py +8 -0
- realign/dashboard/app.py +68 -6
- realign/dashboard/backends/__init__.py +6 -0
- realign/dashboard/backends/iterm2.py +599 -0
- realign/dashboard/backends/kitty.py +372 -0
- realign/dashboard/layout.py +320 -0
- realign/dashboard/terminal_backend.py +110 -0
- realign/dashboard/widgets/events_table.py +44 -5
- realign/dashboard/widgets/sessions_table.py +76 -16
- realign/dashboard/widgets/terminal_panel.py +566 -104
- realign/watcher_daemon.py +11 -0
- realign/worker_daemon.py +11 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Terminal backend interface for native terminal support.
|
|
2
|
+
|
|
3
|
+
This module defines the protocol for terminal backends (iTerm2, Kitty)
|
|
4
|
+
that allow the Aline Dashboard to control native terminal windows
|
|
5
|
+
instead of using tmux for rendering.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class TerminalInfo:
|
|
16
|
+
"""Information about a terminal tab/window managed by a backend."""
|
|
17
|
+
|
|
18
|
+
terminal_id: str # Aline internal ID (UUID)
|
|
19
|
+
session_id: str # Backend-specific session ID (iTerm2 session_id or Kitty window_id)
|
|
20
|
+
name: str # Display name
|
|
21
|
+
active: bool = False # Whether this terminal is currently focused
|
|
22
|
+
claude_session_id: str | None = None # Claude Code session ID if applicable
|
|
23
|
+
context_id: str | None = None # Aline context ID
|
|
24
|
+
provider: str | None = None # Terminal provider (claude, codex, etc.)
|
|
25
|
+
attention: str | None = None # Attention state (permission_request, stop, etc.)
|
|
26
|
+
created_at: float | None = None # Unix timestamp when terminal was created
|
|
27
|
+
metadata: dict[str, str] = field(default_factory=dict) # Additional metadata
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class TerminalBackend(Protocol):
|
|
32
|
+
"""Protocol for terminal backends.
|
|
33
|
+
|
|
34
|
+
Implementations must provide async methods to:
|
|
35
|
+
- Create new terminal tabs
|
|
36
|
+
- Focus/switch to existing tabs
|
|
37
|
+
- Close tabs
|
|
38
|
+
- List all managed tabs
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
async def create_tab(
|
|
42
|
+
self,
|
|
43
|
+
command: str,
|
|
44
|
+
terminal_id: str,
|
|
45
|
+
*,
|
|
46
|
+
name: str | None = None,
|
|
47
|
+
env: dict[str, str] | None = None,
|
|
48
|
+
cwd: str | None = None,
|
|
49
|
+
) -> str | None:
|
|
50
|
+
"""Create a new terminal tab.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
command: The command to run in the new tab
|
|
54
|
+
terminal_id: Aline internal terminal ID
|
|
55
|
+
name: Optional display name for the tab
|
|
56
|
+
env: Optional environment variables to set
|
|
57
|
+
cwd: Optional working directory
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Backend-specific session ID, or None if creation failed
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
async def focus_tab(self, session_id: str, *, steal_focus: bool = False) -> bool:
|
|
65
|
+
"""Switch to/focus a terminal tab.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
session_id: Backend-specific session ID
|
|
69
|
+
steal_focus: If True, also bring the terminal window to front.
|
|
70
|
+
If False, switch tab but keep focus on Dashboard.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if successful, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
async def close_tab(self, session_id: str) -> bool:
|
|
78
|
+
"""Close a terminal tab.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
session_id: Backend-specific session ID
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if successful, False otherwise
|
|
85
|
+
"""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
async def list_tabs(self) -> list[TerminalInfo]:
|
|
89
|
+
"""List all terminal tabs managed by this backend.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of TerminalInfo objects for each managed tab
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
async def is_available(self) -> bool:
|
|
97
|
+
"""Check if this backend is available and usable.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if the backend can be used, False otherwise
|
|
101
|
+
"""
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def get_backend_name(self) -> str:
|
|
105
|
+
"""Get the human-readable name of this backend.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Backend name (e.g., "iTerm2", "Kitty")
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
@@ -232,6 +232,8 @@ class EventsTable(Container):
|
|
|
232
232
|
table.add_column("Share", key="share", width=12)
|
|
233
233
|
table.add_column("Type", key="type", width=8)
|
|
234
234
|
table.add_column("Sessions", key="sessions", width=8)
|
|
235
|
+
table.add_column("Created By", key="created_by", width=10)
|
|
236
|
+
table.add_column("Shared By", key="shared_by", width=10)
|
|
235
237
|
table.add_column("Event ID", key="event_id", width=12)
|
|
236
238
|
table.add_column("Created", key="created", width=10)
|
|
237
239
|
table.cursor_type = "row"
|
|
@@ -352,6 +354,12 @@ class EventsTable(Container):
|
|
|
352
354
|
table.update_cell(
|
|
353
355
|
eid, "sessions", self._format_cell(str(event["sessions"]), eid)
|
|
354
356
|
)
|
|
357
|
+
table.update_cell(
|
|
358
|
+
eid, "created_by", self._format_cell(event.get("created_by", "-"), eid)
|
|
359
|
+
)
|
|
360
|
+
table.update_cell(
|
|
361
|
+
eid, "shared_by", self._format_cell(event.get("shared_by", "-"), eid)
|
|
362
|
+
)
|
|
355
363
|
table.update_cell(
|
|
356
364
|
eid, "event_id", self._format_cell(event["short_id"], eid)
|
|
357
365
|
)
|
|
@@ -736,6 +744,8 @@ class EventsTable(Container):
|
|
|
736
744
|
e.event_type,
|
|
737
745
|
e.created_at,
|
|
738
746
|
e.share_url,
|
|
747
|
+
e.created_by,
|
|
748
|
+
e.shared_by,
|
|
739
749
|
(SELECT COUNT(*) FROM event_sessions WHERE event_id = e.id) AS session_count
|
|
740
750
|
FROM events e
|
|
741
751
|
ORDER BY e.created_at DESC
|
|
@@ -743,8 +753,9 @@ class EventsTable(Container):
|
|
|
743
753
|
""",
|
|
744
754
|
(int(rows_per_page), int(offset)),
|
|
745
755
|
).fetchall()
|
|
746
|
-
|
|
756
|
+
has_new_columns = True
|
|
747
757
|
except Exception:
|
|
758
|
+
# Fallback for older schema without created_by/shared_by
|
|
748
759
|
rows = conn.execute(
|
|
749
760
|
"""
|
|
750
761
|
SELECT
|
|
@@ -759,18 +770,22 @@ class EventsTable(Container):
|
|
|
759
770
|
""",
|
|
760
771
|
(int(rows_per_page), int(offset)),
|
|
761
772
|
).fetchall()
|
|
762
|
-
|
|
773
|
+
has_new_columns = False
|
|
763
774
|
|
|
764
775
|
for i, row in enumerate(rows):
|
|
765
776
|
event_id = row[0]
|
|
766
777
|
title = row[1] or "(no title)"
|
|
767
778
|
event_type = row[2] or "unknown"
|
|
768
779
|
created_at = row[3]
|
|
769
|
-
if
|
|
780
|
+
if has_new_columns:
|
|
770
781
|
share_url = row[4]
|
|
771
|
-
|
|
782
|
+
created_by = row[5]
|
|
783
|
+
shared_by = row[6]
|
|
784
|
+
session_count = row[7]
|
|
772
785
|
else:
|
|
773
786
|
share_url = None
|
|
787
|
+
created_by = None
|
|
788
|
+
shared_by = None
|
|
774
789
|
session_count = row[4]
|
|
775
790
|
|
|
776
791
|
# Format event type
|
|
@@ -789,6 +804,26 @@ class EventsTable(Container):
|
|
|
789
804
|
except Exception:
|
|
790
805
|
created_str = created_at
|
|
791
806
|
|
|
807
|
+
# Look up user names from users table
|
|
808
|
+
created_by_display = "-"
|
|
809
|
+
shared_by_display = "-"
|
|
810
|
+
if created_by:
|
|
811
|
+
try:
|
|
812
|
+
user_row = conn.execute(
|
|
813
|
+
"SELECT user_name FROM users WHERE uid = ?", (created_by,)
|
|
814
|
+
).fetchone()
|
|
815
|
+
created_by_display = user_row[0] if user_row and user_row[0] else created_by[:8] + "..."
|
|
816
|
+
except Exception:
|
|
817
|
+
created_by_display = created_by[:8] + "..." if len(created_by) > 8 else created_by
|
|
818
|
+
if shared_by:
|
|
819
|
+
try:
|
|
820
|
+
user_row = conn.execute(
|
|
821
|
+
"SELECT user_name FROM users WHERE uid = ?", (shared_by,)
|
|
822
|
+
).fetchone()
|
|
823
|
+
shared_by_display = user_row[0] if user_row and user_row[0] else shared_by[:8] + "..."
|
|
824
|
+
except Exception:
|
|
825
|
+
shared_by_display = shared_by[:8] + "..." if len(shared_by) > 8 else shared_by
|
|
826
|
+
|
|
792
827
|
events.append(
|
|
793
828
|
{
|
|
794
829
|
"index": offset + i + 1,
|
|
@@ -799,6 +834,8 @@ class EventsTable(Container):
|
|
|
799
834
|
"sessions": session_count,
|
|
800
835
|
"share_url": share_url,
|
|
801
836
|
"share_id": self._extract_share_id(share_url),
|
|
837
|
+
"created_by": created_by_display,
|
|
838
|
+
"shared_by": shared_by_display,
|
|
802
839
|
"created": created_str,
|
|
803
840
|
}
|
|
804
841
|
)
|
|
@@ -849,7 +886,7 @@ class EventsTable(Container):
|
|
|
849
886
|
if self.wrap_mode and event.get("share_url"):
|
|
850
887
|
share_val = event.get("share_url")
|
|
851
888
|
|
|
852
|
-
# Column order: ✓, #, Title, Share, Type, Sessions, Event ID, Created
|
|
889
|
+
# Column order: ✓, #, Title, Share, Type, Sessions, Created By, Shared By, Event ID, Created
|
|
853
890
|
table.add_row(
|
|
854
891
|
self._checkbox_cell(eid),
|
|
855
892
|
self._format_cell(str(event["index"]), eid),
|
|
@@ -857,6 +894,8 @@ class EventsTable(Container):
|
|
|
857
894
|
self._format_cell(share_val, eid),
|
|
858
895
|
self._format_cell(event["type"], eid),
|
|
859
896
|
self._format_cell(str(event["sessions"]), eid),
|
|
897
|
+
self._format_cell(event.get("created_by", "-"), eid),
|
|
898
|
+
self._format_cell(event.get("shared_by", "-"), eid),
|
|
860
899
|
self._format_cell(event["short_id"], eid),
|
|
861
900
|
self._format_cell(event["created"], eid),
|
|
862
901
|
key=eid,
|
|
@@ -223,6 +223,8 @@ class SessionsTable(Container):
|
|
|
223
223
|
table.add_column("Project", key="project", width=15)
|
|
224
224
|
table.add_column("Source", key="source", width=10)
|
|
225
225
|
table.add_column("Turns", key="turns", width=6)
|
|
226
|
+
table.add_column("Created By", key="created_by", width=10)
|
|
227
|
+
table.add_column("Shared By", key="shared_by", width=10)
|
|
226
228
|
table.add_column("Session ID", key="session_id", width=20)
|
|
227
229
|
table.add_column("Last Activity", key="last_activity", width=12)
|
|
228
230
|
table.cursor_type = "row"
|
|
@@ -361,6 +363,12 @@ class SessionsTable(Container):
|
|
|
361
363
|
table.update_cell(
|
|
362
364
|
sid, "turns", self._format_cell(str(session["turns"]), sid)
|
|
363
365
|
)
|
|
366
|
+
table.update_cell(
|
|
367
|
+
sid, "created_by", self._format_cell(session.get("created_by", "-"), sid)
|
|
368
|
+
)
|
|
369
|
+
table.update_cell(
|
|
370
|
+
sid, "shared_by", self._format_cell(session.get("shared_by", "-"), sid)
|
|
371
|
+
)
|
|
364
372
|
table.update_cell(
|
|
365
373
|
sid, "session_id", self._format_cell(session["short_id"], sid)
|
|
366
374
|
)
|
|
@@ -805,21 +813,43 @@ class SessionsTable(Container):
|
|
|
805
813
|
# Get paginated sessions
|
|
806
814
|
# Use cached total_turns instead of subquery for performance
|
|
807
815
|
offset = (int(page) - 1) * int(rows_per_page)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
816
|
+
try:
|
|
817
|
+
rows = conn.execute(
|
|
818
|
+
"""
|
|
819
|
+
SELECT
|
|
820
|
+
s.id,
|
|
821
|
+
s.session_type,
|
|
822
|
+
s.workspace_path,
|
|
823
|
+
s.session_title,
|
|
824
|
+
s.last_activity_at,
|
|
825
|
+
s.total_turns,
|
|
826
|
+
s.created_by,
|
|
827
|
+
s.shared_by
|
|
828
|
+
FROM sessions s
|
|
829
|
+
ORDER BY s.last_activity_at DESC
|
|
830
|
+
LIMIT ? OFFSET ?
|
|
831
|
+
""",
|
|
832
|
+
(int(rows_per_page), int(offset)),
|
|
833
|
+
).fetchall()
|
|
834
|
+
has_new_columns = True
|
|
835
|
+
except Exception:
|
|
836
|
+
# Fallback for older schema without created_by/shared_by
|
|
837
|
+
rows = conn.execute(
|
|
838
|
+
"""
|
|
839
|
+
SELECT
|
|
840
|
+
s.id,
|
|
841
|
+
s.session_type,
|
|
842
|
+
s.workspace_path,
|
|
843
|
+
s.session_title,
|
|
844
|
+
s.last_activity_at,
|
|
845
|
+
s.total_turns
|
|
846
|
+
FROM sessions s
|
|
847
|
+
ORDER BY s.last_activity_at DESC
|
|
848
|
+
LIMIT ? OFFSET ?
|
|
849
|
+
""",
|
|
850
|
+
(int(rows_per_page), int(offset)),
|
|
851
|
+
).fetchall()
|
|
852
|
+
has_new_columns = False
|
|
823
853
|
|
|
824
854
|
for i, row in enumerate(rows):
|
|
825
855
|
session_id = row[0]
|
|
@@ -828,6 +858,12 @@ class SessionsTable(Container):
|
|
|
828
858
|
title = row[3] or "(no title)"
|
|
829
859
|
last_activity = row[4]
|
|
830
860
|
turn_count = row[5]
|
|
861
|
+
if has_new_columns:
|
|
862
|
+
created_by = row[6]
|
|
863
|
+
shared_by = row[7]
|
|
864
|
+
else:
|
|
865
|
+
created_by = None
|
|
866
|
+
shared_by = None
|
|
831
867
|
|
|
832
868
|
source_map = {
|
|
833
869
|
"claude": "Claude",
|
|
@@ -846,6 +882,26 @@ class SessionsTable(Container):
|
|
|
846
882
|
except Exception:
|
|
847
883
|
activity_str = last_activity
|
|
848
884
|
|
|
885
|
+
# Look up user names from users table
|
|
886
|
+
created_by_display = "-"
|
|
887
|
+
shared_by_display = "-"
|
|
888
|
+
if created_by:
|
|
889
|
+
try:
|
|
890
|
+
user_row = conn.execute(
|
|
891
|
+
"SELECT user_name FROM users WHERE uid = ?", (created_by,)
|
|
892
|
+
).fetchone()
|
|
893
|
+
created_by_display = user_row[0] if user_row and user_row[0] else created_by[:8] + "..."
|
|
894
|
+
except Exception:
|
|
895
|
+
created_by_display = created_by[:8] + "..." if len(created_by) > 8 else created_by
|
|
896
|
+
if shared_by:
|
|
897
|
+
try:
|
|
898
|
+
user_row = conn.execute(
|
|
899
|
+
"SELECT user_name FROM users WHERE uid = ?", (shared_by,)
|
|
900
|
+
).fetchone()
|
|
901
|
+
shared_by_display = user_row[0] if user_row and user_row[0] else shared_by[:8] + "..."
|
|
902
|
+
except Exception:
|
|
903
|
+
shared_by_display = shared_by[:8] + "..." if len(shared_by) > 8 else shared_by
|
|
904
|
+
|
|
849
905
|
sessions.append(
|
|
850
906
|
{
|
|
851
907
|
"index": offset + i + 1,
|
|
@@ -855,6 +911,8 @@ class SessionsTable(Container):
|
|
|
855
911
|
"project": project,
|
|
856
912
|
"turns": turn_count,
|
|
857
913
|
"title": title,
|
|
914
|
+
"created_by": created_by_display,
|
|
915
|
+
"shared_by": shared_by_display,
|
|
858
916
|
"last_activity": activity_str,
|
|
859
917
|
}
|
|
860
918
|
)
|
|
@@ -905,7 +963,7 @@ class SessionsTable(Container):
|
|
|
905
963
|
if self.wrap_mode:
|
|
906
964
|
display_id = sid
|
|
907
965
|
|
|
908
|
-
# Column order: ✓, #, Title, Project, Source, Turns, Session ID, Last Activity
|
|
966
|
+
# Column order: ✓, #, Title, Project, Source, Turns, Created By, Shared By, Session ID, Last Activity
|
|
909
967
|
table.add_row(
|
|
910
968
|
self._checkbox_cell(sid),
|
|
911
969
|
self._format_cell(str(session["index"]), sid),
|
|
@@ -913,6 +971,8 @@ class SessionsTable(Container):
|
|
|
913
971
|
self._format_cell(session["project"], sid),
|
|
914
972
|
self._format_cell(session["source"], sid),
|
|
915
973
|
self._format_cell(str(session["turns"]), sid),
|
|
974
|
+
self._format_cell(session.get("created_by", "-"), sid),
|
|
975
|
+
self._format_cell(session.get("shared_by", "-"), sid),
|
|
916
976
|
self._format_cell(display_id, sid),
|
|
917
977
|
self._format_cell(session["last_activity"], sid),
|
|
918
978
|
key=sid,
|