aline-ai 0.6.6__py3-none-any.whl → 0.6.7__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:
@@ -8,7 +8,6 @@ from .events_table import EventsTable
8
8
  from .config_panel import ConfigPanel
9
9
  from .search_panel import SearchPanel
10
10
  from .openable_table import OpenableDataTable
11
- from .terminal_panel import TerminalPanel
12
11
  from .agents_panel import AgentsPanel
13
12
 
14
13
  __all__ = [
@@ -20,6 +19,5 @@ __all__ = [
20
19
  "ConfigPanel",
21
20
  "SearchPanel",
22
21
  "OpenableDataTable",
23
- "TerminalPanel",
24
22
  "AgentsPanel",
25
23
  ]
@@ -165,7 +165,9 @@ class AgentsPanel(Container, can_focus=True):
165
165
  self._agents: list[dict] = []
166
166
  self._refresh_worker: Optional[Worker] = None
167
167
  self._share_worker: Optional[Worker] = None
168
+ self._sync_worker: Optional[Worker] = None
168
169
  self._share_agent_id: Optional[str] = None
170
+ self._sync_agent_id: Optional[str] = None
169
171
  self._refresh_timer = None
170
172
 
171
173
  def compose(self) -> ComposeResult:
@@ -216,18 +218,27 @@ class AgentsPanel(Container, can_focus=True):
216
218
  agent_infos = db.list_agent_info()
217
219
  active_terminals = db.list_agents(status="active", limit=1000)
218
220
 
219
- # Get tmux windows to retrieve session_id (same as Terminal panel)
221
+ # Latest window links per terminal (V23)
222
+ latest_links = db.list_latest_window_links(limit=2000)
223
+ link_by_terminal = {l.terminal_id: l for l in latest_links if l.terminal_id}
224
+
225
+ # Get tmux windows to retrieve window id and fallback session_id
220
226
  tmux_windows = tmux_manager.list_inner_windows()
221
- # Map terminal_id -> tmux window
222
227
  terminal_to_window = {
223
228
  w.terminal_id: w for w in tmux_windows if w.terminal_id
224
229
  }
225
230
 
226
- # Collect all session_ids from tmux windows for title lookup
227
- session_ids = [
228
- w.session_id for w in tmux_windows if w.session_id and w.terminal_id
229
- ]
230
- # Fetch titles from database (same method as Terminal panel)
231
+ # Collect all session_ids for title lookup
232
+ session_ids: list[str] = []
233
+ for t in active_terminals:
234
+ link = link_by_terminal.get(t.id)
235
+ if link and link.session_id:
236
+ session_ids.append(link.session_id)
237
+ continue
238
+ window = terminal_to_window.get(t.id)
239
+ if window and window.session_id:
240
+ session_ids.append(window.session_id)
241
+
231
242
  titles = self._fetch_session_titles(session_ids)
232
243
 
233
244
  # Map agent_info.id -> list of terminals
@@ -236,16 +247,21 @@ class AgentsPanel(Container, can_focus=True):
236
247
  # Find which agent_info this terminal belongs to
237
248
  agent_info_id = None
238
249
 
250
+ link = link_by_terminal.get(t.id)
251
+
239
252
  # Method 1: Check source field for "agent:{agent_info_id}" format
240
253
  source = t.source or ""
241
254
  if source.startswith("agent:"):
242
- agent_info_id = source[6:] # Extract agent_info_id after "agent:"
255
+ agent_info_id = source[6:]
256
+
257
+ # Method 2: WindowLink agent_id
258
+ if not agent_info_id and link and link.agent_id:
259
+ agent_info_id = link.agent_id
243
260
 
244
- # Method 2: Fallback - check tmux window's session.agent_id
261
+ # Method 3: Fallback - check tmux window's session.agent_id
245
262
  if not agent_info_id:
246
263
  window = terminal_to_window.get(t.id)
247
264
  if window and window.session_id:
248
- # Look up session to get agent_id
249
265
  session = db.get_session_by_id(window.session_id)
250
266
  if session:
251
267
  agent_info_id = session.agent_id
@@ -254,16 +270,18 @@ class AgentsPanel(Container, can_focus=True):
254
270
  if agent_info_id not in agent_to_terminals:
255
271
  agent_to_terminals[agent_info_id] = []
256
272
 
257
- # Get session_id and title from tmux window (same as Terminal panel)
273
+ # Get session_id from windowlink (preferred) or tmux window
258
274
  window = terminal_to_window.get(t.id)
259
- session_id = window.session_id if window else None
275
+ session_id = (
276
+ link.session_id if link and link.session_id else (window.session_id if window else None)
277
+ )
260
278
  title = titles.get(session_id, "") if session_id else ""
261
279
 
262
280
  agent_to_terminals[agent_info_id].append(
263
281
  {
264
282
  "terminal_id": t.id,
265
283
  "session_id": session_id,
266
- "provider": t.provider or "",
284
+ "provider": link.provider if link and link.provider else (t.provider or ""),
267
285
  "session_type": t.session_type or "",
268
286
  "title": title,
269
287
  "cwd": t.cwd or "",
@@ -278,12 +296,15 @@ class AgentsPanel(Container, can_focus=True):
278
296
  "name": info.name,
279
297
  "description": info.description or "",
280
298
  "terminals": terminals,
299
+ "share_url": getattr(info, "share_url", None),
300
+ "last_synced_at": getattr(info, "last_synced_at", None),
281
301
  }
282
302
  )
283
303
  except Exception as e:
284
304
  logger.debug(f"Failed to collect agents: {e}")
285
305
  return agents
286
306
 
307
+
287
308
  def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
288
309
  # Handle refresh worker
289
310
  if self._refresh_worker is not None and event.worker is self._refresh_worker:
@@ -302,6 +323,10 @@ class AgentsPanel(Container, can_focus=True):
302
323
  if self._share_worker is not None and event.worker is self._share_worker:
303
324
  self._handle_share_worker_state_changed(event)
304
325
 
326
+ # Handle sync worker
327
+ if self._sync_worker is not None and event.worker is self._sync_worker:
328
+ self._handle_sync_worker_state_changed(event)
329
+
305
330
  async def _render_agents(self) -> None:
306
331
  async with self._refresh_lock:
307
332
  try:
@@ -339,15 +364,25 @@ class AgentsPanel(Container, can_focus=True):
339
364
  )
340
365
  )
341
366
 
342
- # Share button
343
- await row.mount(
344
- Button(
345
- "Share",
346
- id=f"share-{safe_id}",
347
- name=agent["id"],
348
- classes="agent-share",
367
+ # Share or Sync button (Sync if agent already has a share_url)
368
+ if agent.get("share_url"):
369
+ await row.mount(
370
+ Button(
371
+ "Sync",
372
+ id=f"sync-{safe_id}",
373
+ name=agent["id"],
374
+ classes="agent-share",
375
+ )
376
+ )
377
+ else:
378
+ await row.mount(
379
+ Button(
380
+ "Share",
381
+ id=f"share-{safe_id}",
382
+ name=agent["id"],
383
+ classes="agent-share",
384
+ )
349
385
  )
350
- )
351
386
 
352
387
  # Create terminal button
353
388
  await row.mount(
@@ -500,6 +535,11 @@ class AgentsPanel(Container, can_focus=True):
500
535
  await self._share_agent(agent_id)
501
536
  return
502
537
 
538
+ if btn_id.startswith("sync-"):
539
+ agent_id = event.button.name or ""
540
+ await self._sync_agent(agent_id)
541
+ return
542
+
503
543
  if btn_id.startswith("switch-"):
504
544
  terminal_id = event.button.name or ""
505
545
  await self._switch_to_terminal(terminal_id)
@@ -543,7 +583,13 @@ class AgentsPanel(Container, can_focus=True):
543
583
 
544
584
  def _on_create_result(self, result: dict | None) -> None:
545
585
  if result:
546
- self.app.notify(f"Created: {result.get('name')}", title="Agent")
586
+ if result.get("imported"):
587
+ n = result.get("sessions_imported", 0)
588
+ self.app.notify(
589
+ f"Imported: {result.get('name')} ({n} sessions)", title="Agent"
590
+ )
591
+ else:
592
+ self.app.notify(f"Created: {result.get('name')}", title="Agent")
547
593
  self.refresh_data()
548
594
 
549
595
  async def _create_terminal_for_agent(self, agent_id: str) -> None:
@@ -619,7 +665,7 @@ class AgentsPanel(Container, can_focus=True):
619
665
  try:
620
666
  from ...codex_home import prepare_codex_home
621
667
 
622
- codex_home = prepare_codex_home(terminal_id, agent_id=agent_id)
668
+ codex_home = prepare_codex_home(terminal_id)
623
669
  except Exception:
624
670
  codex_home = None
625
671
 
@@ -1127,3 +1173,76 @@ class AgentsPanel(Container, can_focus=True):
1127
1173
  self.app.notify(
1128
1174
  f"Share failed (exit {exit_code}){suffix}", title="Share", timeout=6
1129
1175
  )
1176
+
1177
+ async def _sync_agent(self, agent_id: str) -> None:
1178
+ """Sync all sessions for an agent with remote share."""
1179
+ if not agent_id:
1180
+ return
1181
+
1182
+ # Check if sync is already in progress
1183
+ if self._sync_worker is not None and self._sync_worker.state in (
1184
+ WorkerState.PENDING,
1185
+ WorkerState.RUNNING,
1186
+ ):
1187
+ return
1188
+
1189
+ self._sync_agent_id = agent_id
1190
+
1191
+ app = self.app
1192
+
1193
+ def progress_callback(message: str) -> None:
1194
+ try:
1195
+ app.call_from_thread(app.notify, message, title="Sync", timeout=3)
1196
+ except Exception:
1197
+ pass
1198
+
1199
+ def work() -> dict:
1200
+ from ...commands.sync_agent import sync_agent_command
1201
+
1202
+ return sync_agent_command(
1203
+ agent_id=agent_id,
1204
+ progress_callback=progress_callback,
1205
+ )
1206
+
1207
+ self.app.notify("Starting sync...", title="Sync", timeout=2)
1208
+ self._sync_worker = self.run_worker(work, thread=True, exit_on_error=False)
1209
+
1210
+ def _handle_sync_worker_state_changed(self, event: Worker.StateChanged) -> None:
1211
+ """Handle sync worker state changes."""
1212
+ if event.state == WorkerState.ERROR:
1213
+ err = self._sync_worker.error if self._sync_worker else "Unknown error"
1214
+ self.app.notify(f"Sync failed: {err}", title="Sync", severity="error")
1215
+ return
1216
+
1217
+ if event.state != WorkerState.SUCCESS:
1218
+ return
1219
+
1220
+ result = self._sync_worker.result if self._sync_worker else {}
1221
+
1222
+ if result.get("success"):
1223
+ pulled = result.get("sessions_pulled", 0)
1224
+ pushed = result.get("sessions_pushed", 0)
1225
+
1226
+ # Copy share URL to clipboard
1227
+ agent_id = self._sync_agent_id
1228
+ share_url = None
1229
+ if agent_id:
1230
+ agent = next((a for a in self._agents if a["id"] == agent_id), None)
1231
+ if agent:
1232
+ share_url = agent.get("share_url")
1233
+
1234
+ if share_url:
1235
+ copied = copy_text(self.app, share_url)
1236
+ suffix = " (link copied)" if copied else ""
1237
+ else:
1238
+ suffix = ""
1239
+
1240
+ self.app.notify(
1241
+ f"Synced: pulled {pulled}, pushed {pushed} session(s){suffix}",
1242
+ title="Sync",
1243
+ timeout=6,
1244
+ )
1245
+ self.refresh_data()
1246
+ else:
1247
+ error = result.get("error", "Unknown error")
1248
+ self.app.notify(f"Sync failed: {error}", title="Sync", severity="error")