aline-ai 0.6.0__py3-none-any.whl → 0.6.2__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.0.dist-info → aline_ai-0.6.2.dist-info}/METADATA +1 -1
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/RECORD +25 -20
- realign/__init__.py +1 -1
- realign/auth.py +21 -0
- realign/claude_hooks/stop_hook.py +35 -0
- realign/claude_hooks/user_prompt_submit_hook.py +5 -0
- realign/cli.py +76 -34
- realign/commands/auth.py +9 -0
- realign/dashboard/app.py +69 -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/screens/create_agent.py +41 -4
- realign/dashboard/terminal_backend.py +110 -0
- realign/dashboard/tmux_manager.py +17 -0
- realign/dashboard/widgets/terminal_panel.py +587 -110
- realign/db/sqlite_db.py +18 -0
- realign/events/session_summarizer.py +17 -2
- realign/watcher_core.py +56 -22
- realign/worker_core.py +2 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.0.dist-info → aline_ai-0.6.2.dist-info}/top_level.txt +0 -0
|
@@ -60,6 +60,23 @@ def _save_claude_permission_mode(mode: str) -> None:
|
|
|
60
60
|
_save_state("claude_permission_mode", mode)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _load_claude_tracking_mode() -> str:
|
|
64
|
+
"""Load the last used Claude tracking mode from state file."""
|
|
65
|
+
try:
|
|
66
|
+
if DASHBOARD_STATE_FILE.exists():
|
|
67
|
+
with open(DASHBOARD_STATE_FILE, "r", encoding="utf-8") as f:
|
|
68
|
+
state = json.load(f)
|
|
69
|
+
return state.get("claude_tracking_mode", "track")
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
return "track"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _save_claude_tracking_mode(mode: str) -> None:
|
|
76
|
+
"""Save the Claude tracking mode to state file."""
|
|
77
|
+
_save_state("claude_tracking_mode", mode)
|
|
78
|
+
|
|
79
|
+
|
|
63
80
|
def _save_state(key: str, value: str) -> None:
|
|
64
81
|
"""Save a key-value pair to the state file."""
|
|
65
82
|
try:
|
|
@@ -75,10 +92,10 @@ def _save_state(key: str, value: str) -> None:
|
|
|
75
92
|
pass
|
|
76
93
|
|
|
77
94
|
|
|
78
|
-
class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
|
|
95
|
+
class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool, bool]]]):
|
|
79
96
|
"""Modal to create a new agent terminal.
|
|
80
97
|
|
|
81
|
-
Returns a tuple of (agent_type, workspace_path, skip_permissions) on success, None on cancel.
|
|
98
|
+
Returns a tuple of (agent_type, workspace_path, skip_permissions, no_track) on success, None on cancel.
|
|
82
99
|
"""
|
|
83
100
|
|
|
84
101
|
BINDINGS = [
|
|
@@ -175,6 +192,7 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
|
|
|
175
192
|
super().__init__()
|
|
176
193
|
self._workspace_path = _load_last_workspace()
|
|
177
194
|
self._permission_mode = _load_claude_permission_mode()
|
|
195
|
+
self._tracking_mode = _load_claude_tracking_mode()
|
|
178
196
|
|
|
179
197
|
def compose(self) -> ComposeResult:
|
|
180
198
|
with Container(id="create-agent-root"):
|
|
@@ -199,6 +217,11 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
|
|
|
199
217
|
yield RadioButton("Normal", id="perm-normal", value=True)
|
|
200
218
|
yield RadioButton("Skip (--dangerously-skip-permissions)", id="perm-skip")
|
|
201
219
|
|
|
220
|
+
yield Label("Tracking", classes="section-label")
|
|
221
|
+
with RadioSet(id="tracking-mode"):
|
|
222
|
+
yield RadioButton("Track", id="track-track", value=True)
|
|
223
|
+
yield RadioButton("No Track (skip LLM summaries)", id="track-notrack")
|
|
224
|
+
|
|
202
225
|
with Horizontal(id="buttons"):
|
|
203
226
|
yield Button("Cancel", id="cancel")
|
|
204
227
|
yield Button("Create", id="create", variant="primary")
|
|
@@ -209,6 +232,11 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
|
|
|
209
232
|
self.query_one("#perm-skip", RadioButton).value = True
|
|
210
233
|
else:
|
|
211
234
|
self.query_one("#perm-normal", RadioButton).value = True
|
|
235
|
+
# Set the saved tracking mode
|
|
236
|
+
if self._tracking_mode == "notrack":
|
|
237
|
+
self.query_one("#track-notrack", RadioButton).value = True
|
|
238
|
+
else:
|
|
239
|
+
self.query_one("#track-track", RadioButton).value = True
|
|
212
240
|
self.query_one("#create", Button).focus()
|
|
213
241
|
|
|
214
242
|
def action_close(self) -> None:
|
|
@@ -292,8 +320,9 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
|
|
|
292
320
|
}
|
|
293
321
|
agent_type = agent_type_map.get(pressed_button.id or "", "claude")
|
|
294
322
|
|
|
295
|
-
# Get permission mode (only relevant for Claude)
|
|
323
|
+
# Get permission mode and tracking mode (only relevant for Claude)
|
|
296
324
|
skip_permissions = False
|
|
325
|
+
no_track = False
|
|
297
326
|
if agent_type == "claude":
|
|
298
327
|
perm_radio_set = self.query_one("#permission-mode", RadioSet)
|
|
299
328
|
perm_pressed = perm_radio_set.pressed_button
|
|
@@ -302,8 +331,16 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
|
|
|
302
331
|
permission_mode = "skip" if skip_permissions else "normal"
|
|
303
332
|
_save_claude_permission_mode(permission_mode)
|
|
304
333
|
|
|
334
|
+
# Get tracking mode
|
|
335
|
+
track_radio_set = self.query_one("#tracking-mode", RadioSet)
|
|
336
|
+
track_pressed = track_radio_set.pressed_button
|
|
337
|
+
no_track = track_pressed is not None and track_pressed.id == "track-notrack"
|
|
338
|
+
# Save the tracking mode for next time
|
|
339
|
+
tracking_mode = "notrack" if no_track else "track"
|
|
340
|
+
_save_claude_tracking_mode(tracking_mode)
|
|
341
|
+
|
|
305
342
|
# Save the workspace path for next time
|
|
306
343
|
_save_last_workspace(self._workspace_path)
|
|
307
344
|
|
|
308
345
|
# Return the result
|
|
309
|
-
self.dismiss((agent_type, self._workspace_path, skip_permissions))
|
|
346
|
+
self.dismiss((agent_type, self._workspace_path, skip_permissions, no_track))
|
|
@@ -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
|
+
...
|
|
@@ -45,6 +45,7 @@ OPT_TRANSCRIPT_PATH = "@aline_transcript_path"
|
|
|
45
45
|
OPT_CONTEXT_ID = "@aline_context_id"
|
|
46
46
|
OPT_ATTENTION = "@aline_attention"
|
|
47
47
|
OPT_CREATED_AT = "@aline_created_at"
|
|
48
|
+
OPT_NO_TRACK = "@aline_no_track"
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
@dataclass(frozen=True)
|
|
@@ -60,6 +61,7 @@ class InnerWindow:
|
|
|
60
61
|
context_id: str | None = None
|
|
61
62
|
attention: str | None = None # "permission_request", "stop", or None
|
|
62
63
|
created_at: float | None = None # Unix timestamp when window was created
|
|
64
|
+
no_track: bool = False # Whether tracking is disabled for this terminal
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
def tmux_available() -> bool:
|
|
@@ -604,6 +606,8 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
604
606
|
+ OPT_ATTENTION
|
|
605
607
|
+ "}\t#{"
|
|
606
608
|
+ OPT_CREATED_AT
|
|
609
|
+
+ "}\t#{"
|
|
610
|
+
+ OPT_NO_TRACK
|
|
607
611
|
+ "}",
|
|
608
612
|
],
|
|
609
613
|
capture=True,
|
|
@@ -632,6 +636,8 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
632
636
|
created_at = float(created_at_str)
|
|
633
637
|
except ValueError:
|
|
634
638
|
pass
|
|
639
|
+
no_track_str = parts[11] if len(parts) > 11 and parts[11] else None
|
|
640
|
+
no_track = no_track_str == "1"
|
|
635
641
|
|
|
636
642
|
if terminal_id:
|
|
637
643
|
persisted = state.get(terminal_id) or {}
|
|
@@ -663,6 +669,7 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
663
669
|
context_id=context_id,
|
|
664
670
|
attention=attention,
|
|
665
671
|
created_at=created_at,
|
|
672
|
+
no_track=no_track,
|
|
666
673
|
)
|
|
667
674
|
)
|
|
668
675
|
# Sort by creation time (newest first). Windows without created_at go to the bottom.
|
|
@@ -758,6 +765,16 @@ def select_inner_window(window_id: str) -> bool:
|
|
|
758
765
|
return _run_inner_tmux(["select-window", "-t", window_id]).returncode == 0
|
|
759
766
|
|
|
760
767
|
|
|
768
|
+
def focus_right_pane() -> bool:
|
|
769
|
+
"""Focus the right pane (terminal area) in the outer tmux layout."""
|
|
770
|
+
return (
|
|
771
|
+
_run_outer_tmux(
|
|
772
|
+
["select-pane", "-t", f"{OUTER_SESSION}:{OUTER_WINDOW}.1"]
|
|
773
|
+
).returncode
|
|
774
|
+
== 0
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
|
|
761
778
|
def clear_attention(window_id: str) -> bool:
|
|
762
779
|
"""Clear the attention state for a window (e.g., after user acknowledges permission request)."""
|
|
763
780
|
if not ensure_inner_session():
|