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.
@@ -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: 60;
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: 2;
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="Agent name (leave blank for random)", id="agent-name")
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
- await self._create_agent()
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
- description = self.query_one("#agent-description", Input).value.strip()
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
- if not provider:
780
- provider = persisted.get("provider") or provider
781
- if not session_type:
782
- session_type = persisted.get("session_type") or session_type
783
- if not session_id:
784
- session_id = persisted.get("session_id") or session_id
785
- if not transcript_path:
786
- transcript_path = persisted.get("transcript_path") or transcript_path
787
- if not context_id:
788
- context_id = persisted.get("context_id") or context_id
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
  ]