aline-ai 0.7.1__py3-none-any.whl → 0.7.3__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.7.1.dist-info → aline_ai-0.7.3.dist-info}/METADATA +1 -1
- {aline_ai-0.7.1.dist-info → aline_ai-0.7.3.dist-info}/RECORD +17 -15
- realign/__init__.py +1 -1
- realign/commands/export_shares.py +191 -65
- realign/commands/sync_agent.py +55 -1
- realign/config.py +6 -1
- realign/dashboard/app.py +28 -36
- realign/dashboard/local_api.py +122 -0
- realign/dashboard/screens/create_agent.py +2 -11
- realign/dashboard/state.py +41 -0
- realign/dashboard/tmux_manager.py +15 -14
- realign/dashboard/widgets/agents_panel.py +264 -209
- realign/dashboard/widgets/config_panel.py +63 -1
- {aline_ai-0.7.1.dist-info → aline_ai-0.7.3.dist-info}/WHEEL +0 -0
- {aline_ai-0.7.1.dist-info → aline_ai-0.7.3.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.7.1.dist-info → aline_ai-0.7.3.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.7.1.dist-info → aline_ai-0.7.3.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ import asyncio
|
|
|
6
6
|
import json as _json
|
|
7
7
|
import re
|
|
8
8
|
import shlex
|
|
9
|
+
import time
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Optional
|
|
11
12
|
|
|
@@ -171,6 +172,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
171
172
|
self._share_agent_id: Optional[str] = None
|
|
172
173
|
self._sync_agent_id: Optional[str] = None
|
|
173
174
|
self._refresh_timer = None
|
|
175
|
+
self._last_refresh_error_at: float | None = None
|
|
174
176
|
|
|
175
177
|
def compose(self) -> ComposeResult:
|
|
176
178
|
with Horizontal(classes="summary"):
|
|
@@ -180,7 +182,8 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
180
182
|
|
|
181
183
|
def on_show(self) -> None:
|
|
182
184
|
if self._refresh_timer is None:
|
|
183
|
-
|
|
185
|
+
# Refresh frequently, but avoid hammering SQLite/tmux (can cause transient empty UI).
|
|
186
|
+
self._refresh_timer = self.set_interval(2.0, self._on_refresh_timer)
|
|
184
187
|
else:
|
|
185
188
|
try:
|
|
186
189
|
self._refresh_timer.resume()
|
|
@@ -212,100 +215,127 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
212
215
|
|
|
213
216
|
def _collect_agents(self) -> list[dict]:
|
|
214
217
|
"""Collect agent info with their terminals."""
|
|
215
|
-
agents = []
|
|
216
|
-
try:
|
|
217
|
-
from ...db import get_database
|
|
218
|
+
agents: list[dict] = []
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
from ...db import get_database
|
|
221
|
+
|
|
222
|
+
# Dashboard should prefer correctness/stability over ultra-low lock timeouts.
|
|
223
|
+
db = get_database(read_only=True, connect_timeout_seconds=2.0)
|
|
224
|
+
|
|
225
|
+
# Critical: if this fails, let it surface as a worker ERROR so we can keep
|
|
226
|
+
# the last rendered UI instead of flashing an empty agent list.
|
|
227
|
+
agent_infos = db.list_agent_info()
|
|
228
|
+
|
|
229
|
+
# Best-effort: missing pieces should degrade gracefully (names still render).
|
|
230
|
+
try:
|
|
221
231
|
active_terminals = db.list_agents(status="active", limit=1000)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.debug(f"Failed to list active terminals: {e}")
|
|
234
|
+
active_terminals = []
|
|
222
235
|
|
|
223
|
-
|
|
236
|
+
try:
|
|
224
237
|
latest_links = db.list_latest_window_links(limit=2000)
|
|
225
|
-
|
|
238
|
+
except Exception:
|
|
239
|
+
latest_links = []
|
|
240
|
+
link_by_terminal = {
|
|
241
|
+
l.terminal_id: l for l in latest_links if getattr(l, "terminal_id", None)
|
|
242
|
+
}
|
|
226
243
|
|
|
227
|
-
|
|
244
|
+
try:
|
|
228
245
|
tmux_windows = tmux_manager.list_inner_windows()
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.debug(f"Failed to list tmux windows: {e}")
|
|
248
|
+
tmux_windows = []
|
|
249
|
+
terminal_to_window = {
|
|
250
|
+
w.terminal_id: w for w in tmux_windows if getattr(w, "terminal_id", None)
|
|
251
|
+
}
|
|
232
252
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
253
|
+
# Collect all session_ids for title lookup
|
|
254
|
+
session_ids: list[str] = []
|
|
255
|
+
for t in active_terminals:
|
|
256
|
+
link = link_by_terminal.get(t.id)
|
|
257
|
+
if link and getattr(link, "session_id", None):
|
|
258
|
+
session_ids.append(link.session_id)
|
|
259
|
+
continue
|
|
260
|
+
window = terminal_to_window.get(t.id)
|
|
261
|
+
if window and getattr(window, "session_id", None):
|
|
262
|
+
session_ids.append(window.session_id)
|
|
263
|
+
|
|
264
|
+
titles = self._fetch_session_titles(session_ids)
|
|
265
|
+
|
|
266
|
+
# Map agent_info.id -> list of terminals
|
|
267
|
+
agent_to_terminals: dict[str, list[dict]] = {}
|
|
268
|
+
for t in active_terminals:
|
|
269
|
+
# Find which agent_info this terminal belongs to
|
|
270
|
+
agent_info_id = None
|
|
271
|
+
|
|
272
|
+
link = link_by_terminal.get(t.id)
|
|
273
|
+
|
|
274
|
+
# Method 1: Check source field for "agent:{agent_info_id}" format
|
|
275
|
+
source = t.source or ""
|
|
276
|
+
if source.startswith("agent:"):
|
|
277
|
+
agent_info_id = source[6:]
|
|
278
|
+
|
|
279
|
+
# Method 2: WindowLink agent_id
|
|
280
|
+
if not agent_info_id and link and getattr(link, "agent_id", None):
|
|
281
|
+
agent_info_id = link.agent_id
|
|
282
|
+
|
|
283
|
+
# Method 3: Fallback - check tmux window's session.agent_id
|
|
284
|
+
if not agent_info_id:
|
|
240
285
|
window = terminal_to_window.get(t.id)
|
|
241
|
-
if window and window
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
titles = self._fetch_session_titles(session_ids)
|
|
245
|
-
|
|
246
|
-
# Map agent_info.id -> list of terminals
|
|
247
|
-
agent_to_terminals: dict[str, list[dict]] = {}
|
|
248
|
-
for t in active_terminals:
|
|
249
|
-
# Find which agent_info this terminal belongs to
|
|
250
|
-
agent_info_id = None
|
|
251
|
-
|
|
252
|
-
link = link_by_terminal.get(t.id)
|
|
253
|
-
|
|
254
|
-
# Method 1: Check source field for "agent:{agent_info_id}" format
|
|
255
|
-
source = t.source or ""
|
|
256
|
-
if source.startswith("agent:"):
|
|
257
|
-
agent_info_id = source[6:]
|
|
258
|
-
|
|
259
|
-
# Method 2: WindowLink agent_id
|
|
260
|
-
if not agent_info_id and link and link.agent_id:
|
|
261
|
-
agent_info_id = link.agent_id
|
|
262
|
-
|
|
263
|
-
# Method 3: Fallback - check tmux window's session.agent_id
|
|
264
|
-
if not agent_info_id:
|
|
265
|
-
window = terminal_to_window.get(t.id)
|
|
266
|
-
if window and window.session_id:
|
|
286
|
+
if window and getattr(window, "session_id", None):
|
|
287
|
+
try:
|
|
267
288
|
session = db.get_session_by_id(window.session_id)
|
|
268
|
-
|
|
269
|
-
|
|
289
|
+
except Exception:
|
|
290
|
+
session = None
|
|
291
|
+
if session:
|
|
292
|
+
agent_info_id = session.agent_id
|
|
270
293
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
agent_to_terminals[agent_info_id] = []
|
|
294
|
+
if agent_info_id:
|
|
295
|
+
agent_to_terminals.setdefault(agent_info_id, [])
|
|
274
296
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"terminal_id": t.id,
|
|
285
|
-
"session_id": session_id,
|
|
286
|
-
"provider": link.provider if link and link.provider else (t.provider or ""),
|
|
287
|
-
"session_type": t.session_type or "",
|
|
288
|
-
"title": title,
|
|
289
|
-
"cwd": t.cwd or "",
|
|
290
|
-
}
|
|
297
|
+
# Get session_id from windowlink (preferred) or tmux window
|
|
298
|
+
window = terminal_to_window.get(t.id)
|
|
299
|
+
session_id = (
|
|
300
|
+
link.session_id
|
|
301
|
+
if link and getattr(link, "session_id", None)
|
|
302
|
+
else (
|
|
303
|
+
window.session_id
|
|
304
|
+
if window and getattr(window, "session_id", None)
|
|
305
|
+
else None
|
|
291
306
|
)
|
|
307
|
+
)
|
|
308
|
+
title = titles.get(session_id, "") if session_id else ""
|
|
292
309
|
|
|
293
|
-
|
|
294
|
-
terminals = agent_to_terminals.get(info.id, [])
|
|
295
|
-
agents.append(
|
|
310
|
+
agent_to_terminals[agent_info_id].append(
|
|
296
311
|
{
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
"terminal_id": t.id,
|
|
313
|
+
"session_id": session_id,
|
|
314
|
+
"provider": (
|
|
315
|
+
link.provider
|
|
316
|
+
if link and getattr(link, "provider", None)
|
|
317
|
+
else (t.provider or "")
|
|
318
|
+
),
|
|
319
|
+
"session_type": t.session_type or "",
|
|
320
|
+
"title": title,
|
|
321
|
+
"cwd": t.cwd or "",
|
|
303
322
|
}
|
|
304
323
|
)
|
|
305
|
-
except Exception as e:
|
|
306
|
-
logger.debug(f"Failed to collect agents: {e}")
|
|
307
|
-
return agents
|
|
308
324
|
|
|
325
|
+
for info in agent_infos:
|
|
326
|
+
terminals = agent_to_terminals.get(info.id, [])
|
|
327
|
+
agents.append(
|
|
328
|
+
{
|
|
329
|
+
"id": info.id,
|
|
330
|
+
"name": info.name,
|
|
331
|
+
"description": info.description or "",
|
|
332
|
+
"terminals": terminals,
|
|
333
|
+
"share_url": getattr(info, "share_url", None),
|
|
334
|
+
"last_synced_at": getattr(info, "last_synced_at", None),
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return agents
|
|
309
339
|
|
|
310
340
|
@staticmethod
|
|
311
341
|
def _fingerprint(agents: list[dict]) -> str:
|
|
@@ -319,9 +349,21 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
319
349
|
# Handle refresh worker
|
|
320
350
|
if self._refresh_worker is not None and event.worker is self._refresh_worker:
|
|
321
351
|
if event.state == WorkerState.ERROR:
|
|
322
|
-
|
|
352
|
+
# Keep the last successfully-rendered list on refresh errors to avoid
|
|
353
|
+
# flashing an empty Agents tab during transient tmux/SQLite hiccups.
|
|
354
|
+
self._last_refresh_error_at = time.monotonic()
|
|
355
|
+
err = self._refresh_worker.error
|
|
356
|
+
if isinstance(err, BaseException):
|
|
357
|
+
logger.warning(
|
|
358
|
+
"Agents refresh failed",
|
|
359
|
+
exc_info=(type(err), err, err.__traceback__),
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
logger.warning(f"Agents refresh failed: {err}")
|
|
363
|
+
return
|
|
323
364
|
elif event.state == WorkerState.SUCCESS:
|
|
324
365
|
self._agents = self._refresh_worker.result or []
|
|
366
|
+
self._last_refresh_error_at = None
|
|
325
367
|
else:
|
|
326
368
|
return
|
|
327
369
|
fp = self._fingerprint(self._agents)
|
|
@@ -329,7 +371,10 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
329
371
|
return # nothing changed – skip re-render to avoid flicker
|
|
330
372
|
self._rendered_fingerprint = fp
|
|
331
373
|
self.run_worker(
|
|
332
|
-
self._render_agents(),
|
|
374
|
+
self._render_agents(),
|
|
375
|
+
group="agents-render",
|
|
376
|
+
exclusive=True,
|
|
377
|
+
exit_on_error=False,
|
|
333
378
|
)
|
|
334
379
|
return
|
|
335
380
|
|
|
@@ -345,108 +390,123 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
345
390
|
async with self._refresh_lock:
|
|
346
391
|
try:
|
|
347
392
|
container = self.query_one("#agents-list", Vertical)
|
|
348
|
-
|
|
349
|
-
return
|
|
393
|
+
await container.remove_children()
|
|
350
394
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
await container.mount(
|
|
355
|
-
Static("No agents yet. Click 'Create Agent' to add one.")
|
|
356
|
-
)
|
|
357
|
-
return
|
|
395
|
+
if not self._agents:
|
|
396
|
+
await container.mount(Static("No agents yet. Click 'Create Agent' to add one."))
|
|
397
|
+
return
|
|
358
398
|
|
|
359
|
-
|
|
360
|
-
|
|
399
|
+
for agent in self._agents:
|
|
400
|
+
safe_id = self._safe_id(agent["id"])
|
|
361
401
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
402
|
+
# Agent row with name, create button, and delete button
|
|
403
|
+
row = Horizontal(classes="agent-row")
|
|
404
|
+
await container.mount(row)
|
|
365
405
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
406
|
+
# Agent name button
|
|
407
|
+
name_label = Text(agent["name"], style="bold")
|
|
408
|
+
terminal_count = len(agent["terminals"])
|
|
409
|
+
if terminal_count > 0:
|
|
410
|
+
name_label.append(f" ({terminal_count})", style="dim")
|
|
371
411
|
|
|
372
|
-
|
|
373
|
-
AgentNameButton(
|
|
412
|
+
agent_btn = AgentNameButton(
|
|
374
413
|
name_label,
|
|
375
414
|
id=f"agent-{safe_id}",
|
|
376
415
|
name=agent["id"],
|
|
377
416
|
classes="agent-name",
|
|
378
417
|
)
|
|
379
|
-
|
|
418
|
+
if agent.get("description"):
|
|
419
|
+
agent_btn.tooltip = agent["description"]
|
|
420
|
+
await row.mount(agent_btn)
|
|
380
421
|
|
|
381
|
-
|
|
382
|
-
|
|
422
|
+
# Share or Sync button (Sync if agent already has a share_url)
|
|
423
|
+
if agent.get("share_url"):
|
|
424
|
+
await row.mount(
|
|
425
|
+
Button(
|
|
426
|
+
"Sync",
|
|
427
|
+
id=f"sync-{safe_id}",
|
|
428
|
+
name=agent["id"],
|
|
429
|
+
classes="agent-share",
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
await row.mount(
|
|
433
|
+
Button(
|
|
434
|
+
"Link",
|
|
435
|
+
id=f"link-{safe_id}",
|
|
436
|
+
name=agent["id"],
|
|
437
|
+
classes="agent-share",
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
else:
|
|
441
|
+
await row.mount(
|
|
442
|
+
Button(
|
|
443
|
+
"Share",
|
|
444
|
+
id=f"share-{safe_id}",
|
|
445
|
+
name=agent["id"],
|
|
446
|
+
classes="agent-share",
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Create terminal button
|
|
383
451
|
await row.mount(
|
|
384
452
|
Button(
|
|
385
|
-
"
|
|
386
|
-
id=f"
|
|
453
|
+
"+ Term",
|
|
454
|
+
id=f"create-term-{safe_id}",
|
|
387
455
|
name=agent["id"],
|
|
388
|
-
classes="agent-
|
|
456
|
+
classes="agent-create",
|
|
389
457
|
)
|
|
390
458
|
)
|
|
391
|
-
|
|
459
|
+
|
|
460
|
+
# Delete agent button
|
|
392
461
|
await row.mount(
|
|
393
462
|
Button(
|
|
394
|
-
"
|
|
395
|
-
id=f"
|
|
463
|
+
"✕",
|
|
464
|
+
id=f"delete-{safe_id}",
|
|
396
465
|
name=agent["id"],
|
|
397
|
-
|
|
466
|
+
variant="error",
|
|
467
|
+
classes="agent-delete",
|
|
398
468
|
)
|
|
399
469
|
)
|
|
400
470
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
)
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
# Terminal list (indented under agent)
|
|
423
|
-
if agent["terminals"]:
|
|
424
|
-
term_list = Vertical(classes="terminal-list")
|
|
425
|
-
await container.mount(term_list)
|
|
426
|
-
|
|
427
|
-
for term in agent["terminals"]:
|
|
428
|
-
term_safe_id = self._safe_id(term["terminal_id"])
|
|
429
|
-
term_row = Horizontal(classes="terminal-row")
|
|
430
|
-
await term_list.mount(term_row)
|
|
431
|
-
|
|
432
|
-
label = self._make_terminal_label(term)
|
|
433
|
-
await term_row.mount(
|
|
434
|
-
Button(
|
|
435
|
-
label,
|
|
436
|
-
id=f"switch-{term_safe_id}",
|
|
437
|
-
name=term["terminal_id"],
|
|
438
|
-
classes="terminal-switch",
|
|
471
|
+
# Terminal list (indented under agent)
|
|
472
|
+
if agent["terminals"]:
|
|
473
|
+
term_list = Vertical(classes="terminal-list")
|
|
474
|
+
await container.mount(term_list)
|
|
475
|
+
|
|
476
|
+
for term in agent["terminals"]:
|
|
477
|
+
term_safe_id = self._safe_id(term["terminal_id"])
|
|
478
|
+
term_row = Horizontal(classes="terminal-row")
|
|
479
|
+
await term_list.mount(term_row)
|
|
480
|
+
|
|
481
|
+
label = self._make_terminal_label(term)
|
|
482
|
+
await term_row.mount(
|
|
483
|
+
Button(
|
|
484
|
+
label,
|
|
485
|
+
id=f"switch-{term_safe_id}",
|
|
486
|
+
name=term["terminal_id"],
|
|
487
|
+
classes="terminal-switch",
|
|
488
|
+
)
|
|
439
489
|
)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
490
|
+
await term_row.mount(
|
|
491
|
+
Button(
|
|
492
|
+
"✕",
|
|
493
|
+
id=f"close-{term_safe_id}",
|
|
494
|
+
name=term["terminal_id"],
|
|
495
|
+
variant="error",
|
|
496
|
+
classes="terminal-close",
|
|
497
|
+
)
|
|
448
498
|
)
|
|
449
|
-
|
|
499
|
+
except Exception:
|
|
500
|
+
logger.exception("Failed to render agents list")
|
|
501
|
+
try:
|
|
502
|
+
container = self.query_one("#agents-list", Vertical)
|
|
503
|
+
await container.remove_children()
|
|
504
|
+
await container.mount(
|
|
505
|
+
Static("Agents UI error (see ~/.aline/.logs/dashboard.log)")
|
|
506
|
+
)
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
return
|
|
450
510
|
|
|
451
511
|
def _make_terminal_label(self, term: dict) -> Text:
|
|
452
512
|
"""Generate label for a terminal."""
|
|
@@ -554,6 +614,11 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
554
614
|
await self._sync_agent(agent_id)
|
|
555
615
|
return
|
|
556
616
|
|
|
617
|
+
if btn_id.startswith("link-"):
|
|
618
|
+
agent_id = event.button.name or ""
|
|
619
|
+
await self._copy_share_link(agent_id)
|
|
620
|
+
return
|
|
621
|
+
|
|
557
622
|
if btn_id.startswith("switch-"):
|
|
558
623
|
terminal_id = event.button.name or ""
|
|
559
624
|
await self._switch_to_terminal(terminal_id)
|
|
@@ -599,9 +664,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
599
664
|
if result:
|
|
600
665
|
if result.get("imported"):
|
|
601
666
|
n = result.get("sessions_imported", 0)
|
|
602
|
-
self.app.notify(
|
|
603
|
-
f"Imported: {result.get('name')} ({n} sessions)", title="Agent"
|
|
604
|
-
)
|
|
667
|
+
self.app.notify(f"Imported: {result.get('name')} ({n} sessions)", title="Agent")
|
|
605
668
|
else:
|
|
606
669
|
self.app.notify(f"Created: {result.get('name')}", title="Agent")
|
|
607
670
|
self.refresh_data()
|
|
@@ -639,11 +702,10 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
639
702
|
|
|
640
703
|
# Create the terminal with agent association
|
|
641
704
|
self.run_worker(
|
|
642
|
-
self._do_create_terminal(
|
|
643
|
-
agent_type, workspace, skip_permissions, no_track, agent_id
|
|
644
|
-
),
|
|
705
|
+
self._do_create_terminal(agent_type, workspace, skip_permissions, no_track, agent_id),
|
|
645
706
|
group="terminal-create",
|
|
646
707
|
exclusive=True,
|
|
708
|
+
exit_on_error=False,
|
|
647
709
|
)
|
|
648
710
|
|
|
649
711
|
async def _do_create_terminal(
|
|
@@ -656,9 +718,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
656
718
|
) -> None:
|
|
657
719
|
"""Actually create the terminal with agent association."""
|
|
658
720
|
if agent_type == "claude":
|
|
659
|
-
await self._create_claude_terminal(
|
|
660
|
-
workspace, skip_permissions, no_track, agent_id
|
|
661
|
-
)
|
|
721
|
+
await self._create_claude_terminal(workspace, skip_permissions, no_track, agent_id)
|
|
662
722
|
elif agent_type == "codex":
|
|
663
723
|
await self._create_codex_terminal(workspace, no_track, agent_id)
|
|
664
724
|
elif agent_type == "opencode":
|
|
@@ -734,13 +794,9 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
734
794
|
except Exception:
|
|
735
795
|
pass
|
|
736
796
|
else:
|
|
737
|
-
self.app.notify(
|
|
738
|
-
"Failed to create terminal", title="Agent", severity="error"
|
|
739
|
-
)
|
|
797
|
+
self.app.notify("Failed to create terminal", title="Agent", severity="error")
|
|
740
798
|
|
|
741
|
-
async def _create_codex_terminal(
|
|
742
|
-
self, workspace: str, no_track: bool, agent_id: str
|
|
743
|
-
) -> None:
|
|
799
|
+
async def _create_codex_terminal(self, workspace: str, no_track: bool, agent_id: str) -> None:
|
|
744
800
|
"""Create a Codex terminal associated with an agent."""
|
|
745
801
|
try:
|
|
746
802
|
from ...db import get_database
|
|
@@ -803,9 +859,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
803
859
|
except Exception:
|
|
804
860
|
pass
|
|
805
861
|
|
|
806
|
-
command = self._command_in_directory(
|
|
807
|
-
tmux_manager.zsh_run_and_keep_open("codex"), workspace
|
|
808
|
-
)
|
|
862
|
+
command = self._command_in_directory(tmux_manager.zsh_run_and_keep_open("codex"), workspace)
|
|
809
863
|
|
|
810
864
|
created = tmux_manager.create_inner_window(
|
|
811
865
|
"codex",
|
|
@@ -817,9 +871,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
817
871
|
)
|
|
818
872
|
|
|
819
873
|
if not created:
|
|
820
|
-
self.app.notify(
|
|
821
|
-
"Failed to create terminal", title="Agent", severity="error"
|
|
822
|
-
)
|
|
874
|
+
self.app.notify("Failed to create terminal", title="Agent", severity="error")
|
|
823
875
|
|
|
824
876
|
async def _create_opencode_terminal(self, workspace: str, agent_id: str) -> None:
|
|
825
877
|
"""Create an Opencode terminal associated with an agent."""
|
|
@@ -878,9 +930,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
878
930
|
except Exception:
|
|
879
931
|
pass
|
|
880
932
|
else:
|
|
881
|
-
self.app.notify(
|
|
882
|
-
"Failed to create terminal", title="Agent", severity="error"
|
|
883
|
-
)
|
|
933
|
+
self.app.notify("Failed to create terminal", title="Agent", severity="error")
|
|
884
934
|
|
|
885
935
|
async def _create_zsh_terminal(self, workspace: str, agent_id: str) -> None:
|
|
886
936
|
"""Create a zsh terminal associated with an agent."""
|
|
@@ -937,9 +987,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
937
987
|
except Exception:
|
|
938
988
|
pass
|
|
939
989
|
else:
|
|
940
|
-
self.app.notify(
|
|
941
|
-
"Failed to create terminal", title="Agent", severity="error"
|
|
942
|
-
)
|
|
990
|
+
self.app.notify("Failed to create terminal", title="Agent", severity="error")
|
|
943
991
|
|
|
944
992
|
def _install_claude_hooks(self, workspace: str) -> None:
|
|
945
993
|
"""Install Claude hooks for a workspace."""
|
|
@@ -966,12 +1014,8 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
966
1014
|
|
|
967
1015
|
project_root = Path(workspace)
|
|
968
1016
|
install_stop_hook(get_stop_settings_path(project_root), quiet=True)
|
|
969
|
-
install_user_prompt_submit_hook(
|
|
970
|
-
|
|
971
|
-
)
|
|
972
|
-
install_permission_request_hook(
|
|
973
|
-
get_permission_settings_path(project_root), quiet=True
|
|
974
|
-
)
|
|
1017
|
+
install_user_prompt_submit_hook(get_submit_settings_path(project_root), quiet=True)
|
|
1018
|
+
install_permission_request_hook(get_permission_settings_path(project_root), quiet=True)
|
|
975
1019
|
except Exception:
|
|
976
1020
|
pass
|
|
977
1021
|
|
|
@@ -1050,14 +1094,10 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
1050
1094
|
db = get_database(read_only=True)
|
|
1051
1095
|
sessions = db.get_sessions_by_agent_id(agent_id)
|
|
1052
1096
|
if not sessions:
|
|
1053
|
-
self.app.notify(
|
|
1054
|
-
"Agent has no sessions to share", title="Share", severity="warning"
|
|
1055
|
-
)
|
|
1097
|
+
self.app.notify("Agent has no sessions to share", title="Share", severity="warning")
|
|
1056
1098
|
return
|
|
1057
1099
|
except Exception as e:
|
|
1058
|
-
self.app.notify(
|
|
1059
|
-
f"Failed to check sessions: {e}", title="Share", severity="error"
|
|
1060
|
-
)
|
|
1100
|
+
self.app.notify(f"Failed to check sessions: {e}", title="Share", severity="error")
|
|
1061
1101
|
return
|
|
1062
1102
|
|
|
1063
1103
|
# Store agent_id for the worker callback
|
|
@@ -1114,9 +1154,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
1114
1154
|
try:
|
|
1115
1155
|
match = re.search(r"\{.*\}", output, re.DOTALL)
|
|
1116
1156
|
if match:
|
|
1117
|
-
result["json"] = json_module.loads(
|
|
1118
|
-
match.group(0), strict=False
|
|
1119
|
-
)
|
|
1157
|
+
result["json"] = json_module.loads(match.group(0), strict=False)
|
|
1120
1158
|
except Exception:
|
|
1121
1159
|
result["json"] = None
|
|
1122
1160
|
|
|
@@ -1151,9 +1189,7 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
1151
1189
|
share_link = payload.get("share_link") or payload.get("share_url")
|
|
1152
1190
|
if not share_link:
|
|
1153
1191
|
share_link = result.get("share_link_guess")
|
|
1154
|
-
slack_message = (
|
|
1155
|
-
payload.get("slack_message") if isinstance(payload, dict) else None
|
|
1156
|
-
)
|
|
1192
|
+
slack_message = payload.get("slack_message") if isinstance(payload, dict) else None
|
|
1157
1193
|
if not slack_message:
|
|
1158
1194
|
try:
|
|
1159
1195
|
from ...db import get_database
|
|
@@ -1176,17 +1212,13 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
1176
1212
|
copied = copy_text(self.app, text_to_copy)
|
|
1177
1213
|
|
|
1178
1214
|
suffix = " (copied)" if copied else ""
|
|
1179
|
-
self.app.notify(
|
|
1180
|
-
f"Share link: {share_link}{suffix}", title="Share", timeout=6
|
|
1181
|
-
)
|
|
1215
|
+
self.app.notify(f"Share link: {share_link}{suffix}", title="Share", timeout=6)
|
|
1182
1216
|
elif exit_code == 0:
|
|
1183
1217
|
self.app.notify("Share completed", title="Share", timeout=3)
|
|
1184
1218
|
else:
|
|
1185
1219
|
extra = result.get("stderr") or ""
|
|
1186
1220
|
suffix = f": {extra}" if extra else ""
|
|
1187
|
-
self.app.notify(
|
|
1188
|
-
f"Share failed (exit {exit_code}){suffix}", title="Share", timeout=6
|
|
1189
|
-
)
|
|
1221
|
+
self.app.notify(f"Share failed (exit {exit_code}){suffix}", title="Share", timeout=6)
|
|
1190
1222
|
|
|
1191
1223
|
async def _sync_agent(self, agent_id: str) -> None:
|
|
1192
1224
|
"""Sync all sessions for an agent with remote share."""
|
|
@@ -1260,3 +1292,26 @@ class AgentsPanel(Container, can_focus=True):
|
|
|
1260
1292
|
else:
|
|
1261
1293
|
error = result.get("error", "Unknown error")
|
|
1262
1294
|
self.app.notify(f"Sync failed: {error}", title="Sync", severity="error")
|
|
1295
|
+
|
|
1296
|
+
async def _copy_share_link(self, agent_id: str) -> None:
|
|
1297
|
+
"""Copy the share link for an agent to clipboard."""
|
|
1298
|
+
if not agent_id:
|
|
1299
|
+
return
|
|
1300
|
+
|
|
1301
|
+
agent = next((a for a in self._agents if a["id"] == agent_id), None)
|
|
1302
|
+
if not agent:
|
|
1303
|
+
self.app.notify("Agent not found", title="Link", severity="error")
|
|
1304
|
+
return
|
|
1305
|
+
|
|
1306
|
+
share_url = agent.get("share_url")
|
|
1307
|
+
if not share_url:
|
|
1308
|
+
self.app.notify("No share link available", title="Link", severity="warning")
|
|
1309
|
+
return
|
|
1310
|
+
|
|
1311
|
+
copied = copy_text(self.app, share_url)
|
|
1312
|
+
if copied:
|
|
1313
|
+
self.app.notify("Share link copied to clipboard", title="Link", timeout=3)
|
|
1314
|
+
else:
|
|
1315
|
+
self.app.notify(
|
|
1316
|
+
f"Failed to copy. Link: {share_url}", title="Link", severity="warning"
|
|
1317
|
+
)
|