aline-ai 0.6.6__py3-none-any.whl → 0.7.0__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.6.dist-info → aline_ai-0.7.0.dist-info}/METADATA +1 -1
- {aline_ai-0.6.6.dist-info → aline_ai-0.7.0.dist-info}/RECORD +28 -28
- realign/__init__.py +1 -1
- realign/agent_names.py +2 -2
- realign/claude_hooks/terminal_state.py +32 -1
- realign/cli.py +2 -4
- realign/codex_detector.py +17 -2
- realign/codex_home.py +24 -6
- realign/commands/auth.py +20 -0
- realign/commands/doctor.py +74 -1
- realign/commands/export_shares.py +151 -0
- realign/commands/import_shares.py +203 -1
- realign/commands/sync_agent.py +347 -0
- realign/dashboard/app.py +3 -53
- realign/dashboard/screens/create_agent_info.py +131 -20
- realign/dashboard/styles/dashboard.tcss +0 -73
- realign/dashboard/tmux_manager.py +36 -10
- realign/dashboard/widgets/__init__.py +0 -6
- realign/dashboard/widgets/agents_panel.py +157 -24
- realign/db/base.py +43 -1
- realign/db/schema.py +60 -2
- realign/db/sqlite_db.py +176 -1
- realign/watcher_core.py +133 -2
- realign/worker_core.py +37 -2
- realign/dashboard/widgets/terminal_panel.py +0 -1688
- {aline_ai-0.6.6.dist-info → aline_ai-0.7.0.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.7.0.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -7,9 +7,10 @@ from typing import Optional
|
|
|
7
7
|
|
|
8
8
|
from textual.app import ComposeResult
|
|
9
9
|
from textual.binding import Binding
|
|
10
|
-
from textual.containers import Container, Horizontal
|
|
10
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
11
11
|
from textual.screen import ModalScreen
|
|
12
12
|
from textual.widgets import Button, Input, Label, Static
|
|
13
|
+
from textual.worker import Worker, WorkerState
|
|
13
14
|
|
|
14
15
|
from ...logging_config import setup_logger
|
|
15
16
|
|
|
@@ -17,8 +18,9 @@ logger = setup_logger("realign.dashboard.screens.create_agent_info", "dashboard.
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class CreateAgentInfoScreen(ModalScreen[Optional[dict]]):
|
|
20
|
-
"""Modal to create a new agent profile.
|
|
21
|
+
"""Modal to create a new agent profile or import from a share link.
|
|
21
22
|
|
|
23
|
+
Both options are shown together; the user picks one.
|
|
22
24
|
Returns a dict with agent info on success, None on cancel.
|
|
23
25
|
"""
|
|
24
26
|
|
|
@@ -32,7 +34,7 @@ class CreateAgentInfoScreen(ModalScreen[Optional[dict]]):
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
CreateAgentInfoScreen #create-agent-info-root {
|
|
35
|
-
width:
|
|
37
|
+
width: 65;
|
|
36
38
|
height: auto;
|
|
37
39
|
max-height: 80%;
|
|
38
40
|
padding: 1 2;
|
|
@@ -54,41 +56,92 @@ class CreateAgentInfoScreen(ModalScreen[Optional[dict]]):
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
CreateAgentInfoScreen Input {
|
|
57
|
-
height: auto;
|
|
58
59
|
margin-top: 0;
|
|
60
|
+
border: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
CreateAgentInfoScreen #or-separator {
|
|
64
|
+
height: auto;
|
|
65
|
+
margin-top: 1;
|
|
66
|
+
margin-bottom: 0;
|
|
67
|
+
text-align: center;
|
|
68
|
+
color: $text-muted;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
CreateAgentInfoScreen #import-status {
|
|
72
|
+
height: auto;
|
|
73
|
+
margin-top: 1;
|
|
74
|
+
color: $text-muted;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
CreateAgentInfoScreen #create-buttons {
|
|
78
|
+
height: auto;
|
|
79
|
+
margin-top: 1;
|
|
80
|
+
align: right middle;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
CreateAgentInfoScreen #buttons {
|
|
83
|
+
CreateAgentInfoScreen #import-buttons {
|
|
62
84
|
height: auto;
|
|
63
|
-
margin-top:
|
|
85
|
+
margin-top: 1;
|
|
64
86
|
align: right middle;
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
CreateAgentInfoScreen #buttons Button {
|
|
89
|
+
CreateAgentInfoScreen #create-buttons Button {
|
|
90
|
+
margin-left: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
CreateAgentInfoScreen #import-buttons Button {
|
|
68
94
|
margin-left: 1;
|
|
69
95
|
}
|
|
70
96
|
"""
|
|
71
97
|
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
super().__init__()
|
|
100
|
+
self._import_worker: Optional[Worker] = None
|
|
101
|
+
from ...agent_names import generate_agent_name
|
|
102
|
+
self._default_name: str = generate_agent_name()
|
|
103
|
+
|
|
72
104
|
def compose(self) -> ComposeResult:
|
|
73
105
|
with Container(id="create-agent-info-root"):
|
|
74
106
|
yield Static("Create Agent Profile", id="create-agent-info-title")
|
|
75
107
|
|
|
108
|
+
# --- Create New section ---
|
|
76
109
|
yield Label("Name", classes="section-label")
|
|
77
|
-
yield Input(placeholder=
|
|
78
|
-
|
|
79
|
-
yield Label("Description", classes="section-label")
|
|
80
|
-
yield Input(placeholder="Optional description", id="agent-description")
|
|
110
|
+
yield Input(placeholder=self._default_name, id="agent-name")
|
|
81
111
|
|
|
82
|
-
with Horizontal(id="buttons"):
|
|
112
|
+
with Horizontal(id="create-buttons"):
|
|
83
113
|
yield Button("Cancel", id="cancel")
|
|
84
114
|
yield Button("Create", id="create", variant="primary")
|
|
85
115
|
|
|
116
|
+
# --- Separator ---
|
|
117
|
+
yield Static("-- Or --", id="or-separator")
|
|
118
|
+
|
|
119
|
+
# --- Import from Link section ---
|
|
120
|
+
yield Label("Import from Link", classes="section-label")
|
|
121
|
+
yield Input(placeholder="https://realign-server.vercel.app/share/...", id="share-url")
|
|
122
|
+
|
|
123
|
+
yield Label("Password (optional)", classes="section-label")
|
|
124
|
+
yield Input(placeholder="Leave blank if not password-protected", id="share-password", password=True)
|
|
125
|
+
|
|
126
|
+
yield Static("", id="import-status")
|
|
127
|
+
|
|
128
|
+
with Horizontal(id="import-buttons"):
|
|
129
|
+
yield Button("Import", id="import", variant="primary")
|
|
130
|
+
|
|
86
131
|
def on_mount(self) -> None:
|
|
87
132
|
self.query_one("#agent-name", Input).focus()
|
|
88
133
|
|
|
89
134
|
def action_close(self) -> None:
|
|
90
135
|
self.dismiss(None)
|
|
91
136
|
|
|
137
|
+
def _set_busy(self, busy: bool) -> None:
|
|
138
|
+
self.query_one("#agent-name", Input).disabled = busy
|
|
139
|
+
self.query_one("#share-url", Input).disabled = busy
|
|
140
|
+
self.query_one("#share-password", Input).disabled = busy
|
|
141
|
+
self.query_one("#create", Button).disabled = busy
|
|
142
|
+
self.query_one("#cancel", Button).disabled = busy
|
|
143
|
+
self.query_one("#import", Button).disabled = busy
|
|
144
|
+
|
|
92
145
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
93
146
|
button_id = event.button.id or ""
|
|
94
147
|
|
|
@@ -100,28 +153,30 @@ class CreateAgentInfoScreen(ModalScreen[Optional[dict]]):
|
|
|
100
153
|
await self._create_agent()
|
|
101
154
|
return
|
|
102
155
|
|
|
156
|
+
if button_id == "import":
|
|
157
|
+
await self._import_agent()
|
|
158
|
+
return
|
|
159
|
+
|
|
103
160
|
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
104
161
|
"""Handle enter key in input fields."""
|
|
105
|
-
|
|
162
|
+
input_id = event.input.id or ""
|
|
163
|
+
if input_id == "agent-name":
|
|
164
|
+
await self._create_agent()
|
|
165
|
+
elif input_id in ("share-url", "share-password"):
|
|
166
|
+
await self._import_agent()
|
|
106
167
|
|
|
107
168
|
async def _create_agent(self) -> None:
|
|
108
169
|
"""Create the agent profile."""
|
|
109
170
|
try:
|
|
110
|
-
from ...agent_names import generate_agent_name
|
|
111
171
|
from ...db import get_database
|
|
112
172
|
|
|
113
173
|
name_input = self.query_one("#agent-name", Input).value.strip()
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# Generate random name if not provided
|
|
117
|
-
name = name_input or generate_agent_name()
|
|
174
|
+
name = name_input or self._default_name
|
|
118
175
|
|
|
119
176
|
agent_id = str(uuid.uuid4())
|
|
120
177
|
|
|
121
178
|
db = get_database(read_only=False)
|
|
122
179
|
record = db.get_or_create_agent_info(agent_id, name=name)
|
|
123
|
-
if description:
|
|
124
|
-
record = db.update_agent_info(agent_id, description=description)
|
|
125
180
|
|
|
126
181
|
self.dismiss({
|
|
127
182
|
"id": record.id,
|
|
@@ -131,3 +186,59 @@ class CreateAgentInfoScreen(ModalScreen[Optional[dict]]):
|
|
|
131
186
|
except Exception as e:
|
|
132
187
|
logger.error(f"Failed to create agent: {e}")
|
|
133
188
|
self.app.notify(f"Failed to create agent: {e}", severity="error")
|
|
189
|
+
|
|
190
|
+
async def _import_agent(self) -> None:
|
|
191
|
+
"""Import an agent from a share link."""
|
|
192
|
+
share_url = self.query_one("#share-url", Input).value.strip()
|
|
193
|
+
password = self.query_one("#share-password", Input).value.strip() or None
|
|
194
|
+
|
|
195
|
+
if not share_url:
|
|
196
|
+
self.app.notify("Please enter a share URL", severity="warning")
|
|
197
|
+
self.query_one("#share-url", Input).focus()
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if "/share/" not in share_url:
|
|
201
|
+
self.app.notify("Invalid share URL format", severity="warning")
|
|
202
|
+
self.query_one("#share-url", Input).focus()
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
status = self.query_one("#import-status", Static)
|
|
206
|
+
status.update("Importing...")
|
|
207
|
+
self._set_busy(True)
|
|
208
|
+
|
|
209
|
+
def do_import() -> dict:
|
|
210
|
+
from ...commands.import_shares import import_agent_from_share
|
|
211
|
+
|
|
212
|
+
return import_agent_from_share(share_url, password=password)
|
|
213
|
+
|
|
214
|
+
self._import_worker = self.run_worker(do_import, thread=True, exit_on_error=False)
|
|
215
|
+
|
|
216
|
+
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
217
|
+
if self._import_worker is None or event.worker is not self._import_worker:
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
status = self.query_one("#import-status", Static)
|
|
221
|
+
|
|
222
|
+
if event.state == WorkerState.ERROR:
|
|
223
|
+
err = self._import_worker.error if self._import_worker else "Unknown error"
|
|
224
|
+
status.update(f"Error: {err}")
|
|
225
|
+
self._set_busy(False)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if event.state != WorkerState.SUCCESS:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
result = self._import_worker.result if self._import_worker else {}
|
|
232
|
+
if not result or not result.get("success"):
|
|
233
|
+
error_msg = (result or {}).get("error", "Import failed")
|
|
234
|
+
status.update(f"Error: {error_msg}")
|
|
235
|
+
self._set_busy(False)
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
self.dismiss({
|
|
239
|
+
"id": result["agent_id"],
|
|
240
|
+
"name": result["agent_name"],
|
|
241
|
+
"description": result.get("agent_description", ""),
|
|
242
|
+
"imported": True,
|
|
243
|
+
"sessions_imported": result.get("sessions_imported", 0),
|
|
244
|
+
})
|
|
@@ -156,79 +156,6 @@ Tab:hover {
|
|
|
156
156
|
background: $surface-lighten-1;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
/* Terminal tab: compact layout without "boxed" buttons/panels. */
|
|
160
|
-
TerminalPanel {
|
|
161
|
-
padding: 0 1;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
TerminalPanel:focus {
|
|
165
|
-
border: none;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
TerminalPanel .summary {
|
|
169
|
-
background: transparent;
|
|
170
|
-
border: none;
|
|
171
|
-
padding: 0;
|
|
172
|
-
margin: 0 0 1 0;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
TerminalPanel .list {
|
|
176
|
-
background: transparent;
|
|
177
|
-
border: none;
|
|
178
|
-
padding: 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
TerminalPanel Button {
|
|
182
|
-
min-width: 0;
|
|
183
|
-
background: transparent;
|
|
184
|
-
border: none;
|
|
185
|
-
padding: 0 1;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
TerminalPanel Button:hover {
|
|
189
|
-
background: $surface-lighten-1;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
TerminalPanel .terminal-row Button.terminal-switch.active {
|
|
193
|
-
background: $primary;
|
|
194
|
-
color: $text;
|
|
195
|
-
text-style: bold;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
TerminalPanel .terminal-row Button.terminal-switch {
|
|
199
|
-
text-align: left;
|
|
200
|
-
content-align: left top;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
TerminalPanel .terminal-row Button.terminal-close {
|
|
204
|
-
padding: 0;
|
|
205
|
-
width: 3;
|
|
206
|
-
min-width: 3;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
TerminalPanel .terminal-row Button.terminal-toggle {
|
|
210
|
-
padding: 0;
|
|
211
|
-
width: 3;
|
|
212
|
-
min-width: 3;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
TerminalPanel .context-sessions {
|
|
216
|
-
border: none;
|
|
217
|
-
padding: 0;
|
|
218
|
-
height: 8;
|
|
219
|
-
overflow-y: auto;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
TerminalPanel Button.context-session {
|
|
223
|
-
text-align: left;
|
|
224
|
-
content-align: left middle;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
TerminalPanel .context-sessions Static {
|
|
228
|
-
text-align: left;
|
|
229
|
-
content-align: left middle;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
159
|
/* Agents tab: compact layout matching terminal panel */
|
|
233
160
|
AgentsPanel {
|
|
234
161
|
padding: 0 1;
|
|
@@ -776,16 +776,42 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
776
776
|
|
|
777
777
|
if terminal_id:
|
|
778
778
|
persisted = state.get(terminal_id) or {}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if
|
|
788
|
-
|
|
779
|
+
updates: dict[str, str] = {}
|
|
780
|
+
persisted_provider = (persisted.get("provider") or "").strip()
|
|
781
|
+
if persisted_provider and persisted_provider != (provider or "").strip():
|
|
782
|
+
provider = persisted_provider
|
|
783
|
+
updates[OPT_PROVIDER] = persisted_provider
|
|
784
|
+
if not provider and persisted_provider:
|
|
785
|
+
provider = persisted_provider
|
|
786
|
+
persisted_session_type = (persisted.get("session_type") or "").strip()
|
|
787
|
+
if persisted_session_type and persisted_session_type != (session_type or "").strip():
|
|
788
|
+
session_type = persisted_session_type
|
|
789
|
+
updates[OPT_SESSION_TYPE] = persisted_session_type
|
|
790
|
+
if not session_type and persisted_session_type:
|
|
791
|
+
session_type = persisted_session_type
|
|
792
|
+
persisted_session_id = (persisted.get("session_id") or "").strip()
|
|
793
|
+
if persisted_session_id and persisted_session_id != (session_id or "").strip():
|
|
794
|
+
session_id = persisted_session_id
|
|
795
|
+
updates[OPT_SESSION_ID] = persisted_session_id
|
|
796
|
+
if not session_id and persisted_session_id:
|
|
797
|
+
session_id = persisted_session_id
|
|
798
|
+
persisted_transcript = (persisted.get("transcript_path") or "").strip()
|
|
799
|
+
if persisted_transcript and persisted_transcript != (transcript_path or "").strip():
|
|
800
|
+
transcript_path = persisted_transcript
|
|
801
|
+
updates[OPT_TRANSCRIPT_PATH] = persisted_transcript
|
|
802
|
+
if not transcript_path and persisted_transcript:
|
|
803
|
+
transcript_path = persisted_transcript
|
|
804
|
+
persisted_context = (persisted.get("context_id") or "").strip()
|
|
805
|
+
if persisted_context and persisted_context != (context_id or "").strip():
|
|
806
|
+
context_id = persisted_context
|
|
807
|
+
updates[OPT_CONTEXT_ID] = persisted_context
|
|
808
|
+
if not context_id and persisted_context:
|
|
809
|
+
context_id = persisted_context
|
|
810
|
+
if updates:
|
|
811
|
+
try:
|
|
812
|
+
set_inner_window_options(window_id, updates)
|
|
813
|
+
except Exception:
|
|
814
|
+
pass
|
|
789
815
|
|
|
790
816
|
transcript_session_id = _session_id_from_transcript_path(transcript_path)
|
|
791
817
|
if transcript_session_id:
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
"""Aline Dashboard Widgets."""
|
|
2
2
|
|
|
3
3
|
from .header import AlineHeader
|
|
4
|
-
from .watcher_panel import WatcherPanel
|
|
5
|
-
from .worker_panel import WorkerPanel
|
|
6
4
|
from .sessions_table import SessionsTable
|
|
7
5
|
from .events_table import EventsTable
|
|
8
6
|
from .config_panel import ConfigPanel
|
|
9
7
|
from .search_panel import SearchPanel
|
|
10
8
|
from .openable_table import OpenableDataTable
|
|
11
|
-
from .terminal_panel import TerminalPanel
|
|
12
9
|
from .agents_panel import AgentsPanel
|
|
13
10
|
|
|
14
11
|
__all__ = [
|
|
15
12
|
"AlineHeader",
|
|
16
|
-
"WatcherPanel",
|
|
17
|
-
"WorkerPanel",
|
|
18
13
|
"SessionsTable",
|
|
19
14
|
"EventsTable",
|
|
20
15
|
"ConfigPanel",
|
|
21
16
|
"SearchPanel",
|
|
22
17
|
"OpenableDataTable",
|
|
23
|
-
"TerminalPanel",
|
|
24
18
|
"AgentsPanel",
|
|
25
19
|
]
|