aline-ai 0.6.5__py3-none-any.whl → 0.6.6__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.
Files changed (38) hide show
  1. {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/RECORD +38 -31
  3. realign/__init__.py +1 -1
  4. realign/agent_names.py +79 -0
  5. realign/claude_hooks/stop_hook.py +3 -0
  6. realign/claude_hooks/terminal_state.py +11 -0
  7. realign/claude_hooks/user_prompt_submit_hook.py +3 -0
  8. realign/cli.py +62 -0
  9. realign/codex_detector.py +1 -1
  10. realign/codex_home.py +46 -15
  11. realign/codex_terminal_linker.py +18 -7
  12. realign/commands/agent.py +109 -0
  13. realign/commands/export_shares.py +297 -0
  14. realign/commands/search.py +58 -29
  15. realign/dashboard/app.py +9 -9
  16. realign/dashboard/clipboard.py +54 -0
  17. realign/dashboard/screens/__init__.py +4 -0
  18. realign/dashboard/screens/agent_detail.py +333 -0
  19. realign/dashboard/screens/create_agent_info.py +133 -0
  20. realign/dashboard/screens/event_detail.py +6 -27
  21. realign/dashboard/styles/dashboard.tcss +67 -0
  22. realign/dashboard/widgets/__init__.py +2 -0
  23. realign/dashboard/widgets/agents_panel.py +1129 -0
  24. realign/dashboard/widgets/events_table.py +4 -27
  25. realign/dashboard/widgets/sessions_table.py +4 -27
  26. realign/dashboard/widgets/terminal_panel.py +40 -5
  27. realign/db/base.py +27 -0
  28. realign/db/locks.py +4 -0
  29. realign/db/schema.py +53 -2
  30. realign/db/sqlite_db.py +185 -2
  31. realign/events/agent_summarizer.py +157 -0
  32. realign/events/session_summarizer.py +25 -0
  33. realign/watcher_core.py +60 -3
  34. realign/worker_core.py +24 -1
  35. {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
  36. {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
  37. {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
  38. {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,109 @@
1
+ """Agent management commands."""
2
+
3
+ import uuid
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+
11
+ def _get_db():
12
+ """Get database instance."""
13
+ from ..config import ReAlignConfig
14
+ from ..db.sqlite_db import SQLiteDatabase
15
+
16
+ config = ReAlignConfig.load()
17
+ db_path = Path(config.sqlite_db_path).expanduser()
18
+ db = SQLiteDatabase(str(db_path))
19
+ db.initialize()
20
+ return db
21
+
22
+
23
+ def agent_new_command(name: str | None = None, desc: str = "") -> int:
24
+ """Create a new agent profile.
25
+
26
+ Args:
27
+ name: Display name (random Docker-style name if None).
28
+ desc: Agent description.
29
+
30
+ Returns:
31
+ Exit code (0 = success).
32
+ """
33
+ from ..agent_names import generate_agent_name
34
+
35
+ agent_id = str(uuid.uuid4())
36
+ display_name = name or generate_agent_name()
37
+
38
+ db = _get_db()
39
+ try:
40
+ record = db.get_or_create_agent_info(agent_id, name=display_name)
41
+ if desc:
42
+ record = db.update_agent_info(agent_id, description=desc)
43
+
44
+ console.print(f"[bold green]Agent created[/bold green]")
45
+ console.print(f" id: {record.id}")
46
+ console.print(f" name: {record.name}")
47
+ console.print(f" description: {record.description or '(none)'}")
48
+ return 0
49
+ finally:
50
+ db.close()
51
+
52
+
53
+ def agent_list_command(*, include_invisible: bool = False) -> int:
54
+ """List agent profiles.
55
+
56
+ Returns:
57
+ Exit code (0 = success).
58
+ """
59
+ from rich.table import Table
60
+
61
+ db = _get_db()
62
+ try:
63
+ agents = db.list_agent_info(include_invisible=include_invisible)
64
+
65
+ if not agents:
66
+ console.print("[dim]No agents yet.[/dim]")
67
+ return 0
68
+
69
+ table = Table(title="Agents")
70
+ table.add_column("ID", style="dim", width=8)
71
+ table.add_column("Name", style="bold")
72
+ table.add_column("Description")
73
+ table.add_column("Sessions", style="cyan")
74
+ table.add_column("Created", style="dim")
75
+
76
+ def _unique_prefixes(ids: list[str], min_len: int = 8) -> list[str]:
77
+ if not ids:
78
+ return []
79
+ max_len = max(len(i) for i in ids)
80
+ length = min_len
81
+ while length <= max_len:
82
+ prefixes = [i[:length] for i in ids]
83
+ if len(set(prefixes)) == len(ids):
84
+ return prefixes
85
+ length += 2
86
+ return ids
87
+
88
+ for agent in agents:
89
+ created_str = agent.created_at.strftime("%Y-%m-%d %H:%M")
90
+ sessions = db.get_sessions_by_agent_id(agent.id)
91
+ if sessions:
92
+ raw_ids = [s.id for s in sessions]
93
+ short_ids = _unique_prefixes(raw_ids, min_len=8)
94
+ session_ids = ", ".join(short_ids)
95
+ sessions_display = f"{len(sessions)} ({session_ids})"
96
+ else:
97
+ sessions_display = "0"
98
+ table.add_row(
99
+ agent.id[:8],
100
+ agent.name,
101
+ agent.description or "",
102
+ sessions_display,
103
+ created_str,
104
+ )
105
+
106
+ console.print(table)
107
+ return 0
108
+ finally:
109
+ db.close()
@@ -15,6 +15,7 @@ import secrets
15
15
  import hashlib
16
16
  import base64
17
17
  import threading
18
+ import shutil
18
19
  from urllib.parse import urlparse
19
20
  from collections import defaultdict
20
21
  from dataclasses import dataclass
@@ -2523,6 +2524,59 @@ def display_share_result(
2523
2524
  console.print(f"[bold]👁️ Max Views:[/bold] {max_views}\n")
2524
2525
 
2525
2526
 
2527
+ def _run_clipboard_command(command: list[str], text: str) -> bool:
2528
+ try:
2529
+ return (
2530
+ subprocess.run(
2531
+ command,
2532
+ input=text,
2533
+ text=True,
2534
+ capture_output=False,
2535
+ check=False,
2536
+ ).returncode
2537
+ == 0
2538
+ )
2539
+ except Exception:
2540
+ return False
2541
+
2542
+
2543
+ def _copy_text_to_clipboard(text: str) -> bool:
2544
+ if not text:
2545
+ return False
2546
+
2547
+ if shutil.which("pbcopy"):
2548
+ if _run_clipboard_command(["pbcopy"], text):
2549
+ return True
2550
+
2551
+ if os.name == "nt" and shutil.which("clip"):
2552
+ if _run_clipboard_command(["clip"], text):
2553
+ return True
2554
+
2555
+ if shutil.which("wl-copy"):
2556
+ if _run_clipboard_command(["wl-copy"], text):
2557
+ return True
2558
+
2559
+ if shutil.which("xclip"):
2560
+ if _run_clipboard_command(["xclip", "-selection", "clipboard"], text):
2561
+ return True
2562
+
2563
+ if shutil.which("xsel"):
2564
+ if _run_clipboard_command(["xsel", "--clipboard", "--input"], text):
2565
+ return True
2566
+
2567
+ return False
2568
+
2569
+
2570
+ def _copy_share_to_clipboard(share_url: Optional[str], slack_message: Optional[str]) -> bool:
2571
+ if not share_url:
2572
+ return False
2573
+ if slack_message:
2574
+ text_to_copy = f"{slack_message}\n\n{share_url}"
2575
+ else:
2576
+ text_to_copy = share_url
2577
+ return _copy_text_to_clipboard(text_to_copy)
2578
+
2579
+
2526
2580
  def _export_by_events_interactive(
2527
2581
  all_commits: List,
2528
2582
  shadow_git: Path,
@@ -3616,7 +3670,250 @@ def export_shares_interactive_command(
3616
3670
  max_views=max_views,
3617
3671
  admin_token=result.get("admin_token"),
3618
3672
  )
3673
+ copied = _copy_share_to_clipboard(
3674
+ result.get("share_url"),
3675
+ ui_metadata.get("slack_message") if ui_metadata else None,
3676
+ )
3677
+ if copied:
3678
+ print("📋 Copied Slack message and share link to clipboard.")
3619
3679
 
3620
3680
  if not json_output:
3621
3681
  logger.info(f"======== Interactive export completed: {result['share_url']} ========")
3622
3682
  return 0
3683
+
3684
+
3685
+ def export_agent_shares_command(
3686
+ agent_id: str,
3687
+ password: Optional[str] = None,
3688
+ expiry_days: int = 7,
3689
+ max_views: int = 100,
3690
+ backend_url: Optional[str] = None,
3691
+ enable_mcp: bool = True,
3692
+ json_output: bool = False,
3693
+ compact: bool = True,
3694
+ max_tool_result_chars: int = 8_000,
3695
+ max_tool_command_chars: int = 2_000,
3696
+ progress_callback: Optional[Callable[[str], None]] = None,
3697
+ ) -> int:
3698
+ """
3699
+ Export all sessions associated with an agent and generate a share link.
3700
+
3701
+ This function creates a synthetic event structure from agent sessions,
3702
+ generates UI metadata (Slack message), uploads to backend, and returns
3703
+ the share link.
3704
+
3705
+ Args:
3706
+ agent_id: The agent_info ID to export sessions for
3707
+ password: Encryption password (if None, no encryption)
3708
+ expiry_days: Share expiry in days
3709
+ max_views: Maximum number of views
3710
+ backend_url: Backend server URL (uses config default if None)
3711
+ enable_mcp: Whether to include MCP instructions
3712
+ json_output: If True, output JSON format
3713
+ compact: Whether to compact the export data
3714
+ max_tool_result_chars: Max chars for tool results (with compact)
3715
+ max_tool_command_chars: Max chars for tool commands (with compact)
3716
+ progress_callback: Optional callback for progress updates (message: str) -> None
3717
+
3718
+ Returns:
3719
+ 0 on success, 1 on error
3720
+ """
3721
+ def _progress(msg: str) -> None:
3722
+ if progress_callback:
3723
+ progress_callback(msg)
3724
+
3725
+ if not json_output:
3726
+ logger.info(f"======== Export agent shares command started for agent {agent_id} ========")
3727
+
3728
+ # Check dependencies
3729
+ if not CRYPTO_AVAILABLE:
3730
+ if not json_output:
3731
+ print("Error: cryptography package not installed", file=sys.stderr)
3732
+ return 1
3733
+
3734
+ if not HTTPX_AVAILABLE:
3735
+ if not json_output:
3736
+ print("Error: httpx package not installed", file=sys.stderr)
3737
+ return 1
3738
+
3739
+ # Check authentication
3740
+ if not is_logged_in():
3741
+ if not json_output:
3742
+ print("Error: Not logged in. Please run 'aline login' first.", file=sys.stderr)
3743
+ return 1
3744
+
3745
+ _progress("Fetching agent info...")
3746
+
3747
+ # Get backend URL
3748
+ if backend_url is None:
3749
+ from ..config import ReAlignConfig
3750
+
3751
+ config = ReAlignConfig.load()
3752
+ backend_url = config.share_backend_url
3753
+
3754
+ # Get database
3755
+ from ..db import get_database
3756
+
3757
+ db = get_database()
3758
+
3759
+ # Get agent info
3760
+ agent_info = db.get_agent_info(agent_id)
3761
+ if not agent_info:
3762
+ if not json_output:
3763
+ print(f"Error: Agent not found: {agent_id}", file=sys.stderr)
3764
+ return 1
3765
+
3766
+ # Get sessions for this agent
3767
+ session_records = db.get_sessions_by_agent_id(agent_id)
3768
+ if not session_records:
3769
+ if not json_output:
3770
+ print(f"Error: Agent has no sessions to share", file=sys.stderr)
3771
+ return 1
3772
+
3773
+ _progress(f"Found {len(session_records)} session(s)")
3774
+
3775
+ # Build exportable sessions
3776
+ selected_sessions = build_exportable_sessions_from_records(session_records)
3777
+ if not selected_sessions:
3778
+ if not json_output:
3779
+ print("Error: No sessions found for agent", file=sys.stderr)
3780
+ return 1
3781
+
3782
+ # Create a synthetic event structure for the agent
3783
+ # We use the agent name/description as event title/description
3784
+ event_title = agent_info.name or "Agent Sessions"
3785
+ event_description = agent_info.description or f"Sessions from agent: {agent_info.name}"
3786
+
3787
+ # Create a synthetic ExportableEvent
3788
+ synthetic_event = ExportableEvent(
3789
+ index=1,
3790
+ event_id=f"agent-{agent_id}",
3791
+ title=event_title,
3792
+ description=event_description,
3793
+ event_type="agent",
3794
+ status="active",
3795
+ updated_at=datetime.now(timezone.utc),
3796
+ sessions=session_records,
3797
+ )
3798
+
3799
+ # Build conversation data
3800
+ _progress("Building conversation data...")
3801
+
3802
+ username = os.environ.get("USER") or os.environ.get("USERNAME") or "anonymous"
3803
+
3804
+ compaction = None
3805
+ if compact:
3806
+ compaction = ExportCompactionConfig(
3807
+ enabled=True,
3808
+ max_tool_result_chars=max_tool_result_chars,
3809
+ max_tool_command_chars=max_tool_command_chars,
3810
+ )
3811
+
3812
+ try:
3813
+ conversation_data = build_enhanced_conversation_data(
3814
+ selected_event=synthetic_event,
3815
+ selected_sessions=selected_sessions,
3816
+ username=username,
3817
+ db=db,
3818
+ compaction=compaction,
3819
+ )
3820
+ except Exception as e:
3821
+ if not json_output:
3822
+ print(f"Error: Failed to build conversation data: {e}", file=sys.stderr)
3823
+ logger.error(f"Failed to build conversation data: {e}", exc_info=True)
3824
+ return 1
3825
+
3826
+ if not conversation_data.get("sessions"):
3827
+ if not json_output:
3828
+ print("Error: No sessions found in conversation data", file=sys.stderr)
3829
+ return 1
3830
+
3831
+ # Generate UI metadata with LLM
3832
+ _progress("Generating share message...")
3833
+
3834
+ from ..config import ReAlignConfig
3835
+
3836
+ config = ReAlignConfig.load()
3837
+ ui_metadata, _ = generate_ui_metadata_with_llm(
3838
+ conversation_data,
3839
+ [], # No commits
3840
+ event_title=event_title,
3841
+ event_description=event_description,
3842
+ provider=config.llm_provider,
3843
+ preset_id="default",
3844
+ silent=json_output,
3845
+ )
3846
+
3847
+ if ui_metadata:
3848
+ conversation_data["ui_metadata"] = ui_metadata
3849
+ else:
3850
+ conversation_data["ui_metadata"] = {
3851
+ "title": event_title,
3852
+ "description": event_description,
3853
+ }
3854
+
3855
+ # Add MCP instructions if enabled
3856
+ if enable_mcp:
3857
+ conversation_data["ui_metadata"]["mcp_instructions"] = {
3858
+ "tool_name": "ask_shared_conversation",
3859
+ "usage": "Local AI agents can install the aline MCP server and use the 'ask_shared_conversation' tool to query this conversation programmatically.",
3860
+ }
3861
+
3862
+ # Upload to backend (no encryption for agent shares by default)
3863
+ _progress("Uploading to cloud...")
3864
+
3865
+ metadata = {
3866
+ "username": username,
3867
+ "expiry_days": expiry_days,
3868
+ "max_views": max_views,
3869
+ }
3870
+
3871
+ try:
3872
+ if password:
3873
+ encrypted_payload = encrypt_conversation_data(conversation_data, password)
3874
+ result = upload_to_backend(
3875
+ encrypted_payload=encrypted_payload,
3876
+ metadata=metadata,
3877
+ backend_url=backend_url,
3878
+ ui_metadata=conversation_data.get("ui_metadata"),
3879
+ background=True,
3880
+ )
3881
+ else:
3882
+ result = upload_to_backend_unencrypted(
3883
+ conversation_data=conversation_data,
3884
+ metadata=metadata,
3885
+ backend_url=backend_url,
3886
+ background=True,
3887
+ )
3888
+ except Exception as e:
3889
+ if not json_output:
3890
+ print(f"Error: Upload failed: {e}", file=sys.stderr)
3891
+ logger.error(f"Upload failed: {e}", exc_info=True)
3892
+ return 1
3893
+
3894
+ share_url = result.get("share_url")
3895
+ slack_message = ui_metadata.get("slack_message") if ui_metadata else None
3896
+
3897
+ # Output results
3898
+ if json_output:
3899
+ output_data = {
3900
+ "agent_id": agent_id,
3901
+ "agent_name": agent_info.name,
3902
+ "share_link": share_url,
3903
+ "slack_message": slack_message,
3904
+ "session_count": len(selected_sessions),
3905
+ "password": password,
3906
+ }
3907
+ print(json.dumps(output_data, ensure_ascii=False, indent=2))
3908
+ else:
3909
+ print(f"\n✅ Shared {len(selected_sessions)} session(s) from agent: {agent_info.name}")
3910
+ print(f"🔗 Share link: {share_url}")
3911
+ if slack_message:
3912
+ print(f"\n📝 Slack message:\n{slack_message}")
3913
+ copied = _copy_share_to_clipboard(share_url, slack_message)
3914
+ if copied:
3915
+ print("📋 Copied Slack message and share link to clipboard.")
3916
+
3917
+ if not json_output:
3918
+ logger.info(f"======== Agent export completed: {share_url} ========")
3919
+ return 0
@@ -335,9 +335,28 @@ def search_command(
335
335
  context_session_ids = get_context_session_ids()
336
336
  context_event_ids = get_context_event_ids()
337
337
 
338
+ # Apply agent scoping if ALINE_AGENT_ID is set
339
+ agent_session_ids = None
340
+ if not no_context:
341
+ import os
342
+
343
+ agent_id = os.environ.get("ALINE_AGENT_ID")
344
+ if agent_id:
345
+ agent_sessions = db.get_sessions_by_agent_id(agent_id)
346
+ # Always set agent_session_ids when agent_id exists
347
+ # (empty list means no sessions for this agent -> empty results)
348
+ agent_session_ids = [s.id for s in agent_sessions]
349
+
338
350
  # Parse session IDs if provided (resolve prefixes)
339
351
  session_ids = _resolve_id_prefixes(db, "sessions", sessions) or None
340
352
 
353
+ # Intersect with agent sessions first (highest priority)
354
+ if agent_session_ids is not None:
355
+ if session_ids:
356
+ session_ids = list(set(session_ids) & set(agent_session_ids))
357
+ else:
358
+ session_ids = agent_session_ids if agent_session_ids else []
359
+
341
360
  # Intersect with context sessions
342
361
  if context_session_ids:
343
362
  if session_ids:
@@ -372,43 +391,53 @@ def search_command(
372
391
  turn_ids = _resolve_id_prefixes(db, "turns", turns) or None
373
392
 
374
393
  # 1. Search Events (events don't have session scope, skip if sessions/events filter is active)
375
- if type in ("all", "event") and not session_ids and not event_ids:
394
+ # Use 'is None' to distinguish "no filter" from "empty filter results"
395
+ if type in ("all", "event") and session_ids is None and event_ids is None:
376
396
  events = db.search_events(query, limit=limit, use_regex=regex, ignore_case=ignore_case)
377
397
  results["events"] = events
378
398
 
379
- # 2. Search Turns
399
+ # 2. Search Turns (skip if session filter results in empty list)
380
400
  if type in ("all", "turn"):
381
- turns = db.search_conversations(
382
- query,
383
- limit=limit,
384
- use_regex=regex,
385
- ignore_case=ignore_case,
386
- session_ids=session_ids,
387
- )
388
- results["turns"] = turns
401
+ if session_ids is not None and len(session_ids) == 0:
402
+ results["turns"] = []
403
+ else:
404
+ turns = db.search_conversations(
405
+ query,
406
+ limit=limit,
407
+ use_regex=regex,
408
+ ignore_case=ignore_case,
409
+ session_ids=session_ids if session_ids else None,
410
+ )
411
+ results["turns"] = turns
389
412
 
390
- # 3. Search Sessions
413
+ # 3. Search Sessions (skip if session filter results in empty list)
391
414
  if type in ("all", "session"):
392
- sessions_results = db.search_sessions(
393
- query,
394
- limit=limit,
395
- use_regex=regex,
396
- ignore_case=ignore_case,
397
- session_ids=session_ids,
398
- )
399
- results["sessions"] = sessions_results
415
+ if session_ids is not None and len(session_ids) == 0:
416
+ results["sessions"] = []
417
+ else:
418
+ sessions_results = db.search_sessions(
419
+ query,
420
+ limit=limit,
421
+ use_regex=regex,
422
+ ignore_case=ignore_case,
423
+ session_ids=session_ids if session_ids else None,
424
+ )
425
+ results["sessions"] = sessions_results
400
426
 
401
- # 4. Search Turn Content
427
+ # 4. Search Turn Content (skip if session filter results in empty list)
402
428
  if type == "content":
403
- content_results = db.search_turn_content(
404
- query,
405
- limit=limit,
406
- use_regex=regex,
407
- ignore_case=ignore_case,
408
- session_ids=session_ids,
409
- turn_ids=turn_ids,
410
- )
411
- results["content"] = content_results
429
+ if session_ids is not None and len(session_ids) == 0:
430
+ results["content"] = []
431
+ else:
432
+ content_results = db.search_turn_content(
433
+ query,
434
+ limit=limit,
435
+ use_regex=regex,
436
+ ignore_case=ignore_case,
437
+ session_ids=session_ids if session_ids else None,
438
+ turn_ids=turn_ids,
439
+ )
440
+ results["content"] = content_results
412
441
 
413
442
  # === Grep-style output for regex mode ===
414
443
  if regex:
realign/dashboard/app.py CHANGED
@@ -16,7 +16,7 @@ from .widgets import (
16
16
  WatcherPanel,
17
17
  WorkerPanel,
18
18
  ConfigPanel,
19
- TerminalPanel,
19
+ AgentsPanel,
20
20
  )
21
21
 
22
22
  # Environment variable to control terminal mode
@@ -104,9 +104,9 @@ class AlineDashboard(App):
104
104
  try:
105
105
  yield AlineHeader()
106
106
  tab_ids = self._tab_ids()
107
- with TabbedContent(initial=tab_ids[0] if tab_ids else "terminal"):
108
- with TabPane("Agents", id="terminal"):
109
- yield TerminalPanel(use_native_terminal=self.use_native_terminal)
107
+ with TabbedContent(initial=tab_ids[0] if tab_ids else "agents"):
108
+ with TabPane("Agents", id="agents"):
109
+ yield AgentsPanel()
110
110
  if self.dev_mode:
111
111
  with TabPane("Watcher", id="watcher"):
112
112
  yield WatcherPanel()
@@ -122,8 +122,8 @@ class AlineDashboard(App):
122
122
 
123
123
  def _tab_ids(self) -> list[str]:
124
124
  if self.dev_mode:
125
- return ["terminal", "watcher", "worker", "config"]
126
- return ["terminal", "config"]
125
+ return ["agents", "watcher", "worker", "config"]
126
+ return ["agents", "config"]
127
127
 
128
128
  def on_mount(self) -> None:
129
129
  """Apply theme based on system settings and watch for changes."""
@@ -205,14 +205,14 @@ class AlineDashboard(App):
205
205
  tabbed_content = self.query_one(TabbedContent)
206
206
  active_tab_id = tabbed_content.active
207
207
 
208
- if active_tab_id == "watcher":
208
+ if active_tab_id == "agents":
209
+ self.query_one(AgentsPanel).refresh_data()
210
+ elif active_tab_id == "watcher":
209
211
  self.query_one(WatcherPanel).refresh_data()
210
212
  elif active_tab_id == "worker":
211
213
  self.query_one(WorkerPanel).refresh_data()
212
214
  elif active_tab_id == "config":
213
215
  self.query_one(ConfigPanel).refresh_data()
214
- elif active_tab_id == "terminal":
215
- await self.query_one(TerminalPanel).refresh_data()
216
216
 
217
217
  def action_page_next(self) -> None:
218
218
  """Go to next page in current panel."""
@@ -0,0 +1,54 @@
1
+ """Clipboard helpers for the dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+
9
+
10
+ def _run_copy(command: list[str], text: str) -> bool:
11
+ try:
12
+ return (
13
+ subprocess.run(
14
+ command,
15
+ input=text,
16
+ text=True,
17
+ capture_output=False,
18
+ check=False,
19
+ ).returncode
20
+ == 0
21
+ )
22
+ except Exception:
23
+ return False
24
+
25
+
26
+ def copy_text(app, text: str) -> bool:
27
+ if not text:
28
+ return False
29
+
30
+ if shutil.which("pbcopy"):
31
+ if _run_copy(["pbcopy"], text):
32
+ return True
33
+
34
+ if os.name == "nt" and shutil.which("clip"):
35
+ if _run_copy(["clip"], text):
36
+ return True
37
+
38
+ if shutil.which("wl-copy"):
39
+ if _run_copy(["wl-copy"], text):
40
+ return True
41
+
42
+ if shutil.which("xclip"):
43
+ if _run_copy(["xclip", "-selection", "clipboard"], text):
44
+ return True
45
+
46
+ if shutil.which("xsel"):
47
+ if _run_copy(["xsel", "--clipboard", "--input"], text):
48
+ return True
49
+
50
+ try:
51
+ app.copy_to_clipboard(text)
52
+ return True
53
+ except Exception:
54
+ return False
@@ -2,16 +2,20 @@
2
2
 
3
3
  from .session_detail import SessionDetailScreen
4
4
  from .event_detail import EventDetailScreen
5
+ from .agent_detail import AgentDetailScreen
5
6
  from .create_event import CreateEventScreen
6
7
  from .create_agent import CreateAgentScreen
8
+ from .create_agent_info import CreateAgentInfoScreen
7
9
  from .share_import import ShareImportScreen
8
10
  from .help_screen import HelpScreen
9
11
 
10
12
  __all__ = [
11
13
  "SessionDetailScreen",
12
14
  "EventDetailScreen",
15
+ "AgentDetailScreen",
13
16
  "CreateEventScreen",
14
17
  "CreateAgentScreen",
18
+ "CreateAgentInfoScreen",
15
19
  "ShareImportScreen",
16
20
  "HelpScreen",
17
21
  ]