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.
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/RECORD +38 -31
- realign/__init__.py +1 -1
- realign/agent_names.py +79 -0
- realign/claude_hooks/stop_hook.py +3 -0
- realign/claude_hooks/terminal_state.py +11 -0
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +1 -1
- realign/codex_home.py +46 -15
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/export_shares.py +297 -0
- realign/commands/search.py +58 -29
- realign/dashboard/app.py +9 -9
- realign/dashboard/clipboard.py +54 -0
- realign/dashboard/screens/__init__.py +4 -0
- realign/dashboard/screens/agent_detail.py +333 -0
- realign/dashboard/screens/create_agent_info.py +133 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +67 -0
- realign/dashboard/widgets/__init__.py +2 -0
- realign/dashboard/widgets/agents_panel.py +1129 -0
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/dashboard/widgets/terminal_panel.py +40 -5
- realign/db/base.py +27 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +53 -2
- realign/db/sqlite_db.py +185 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +60 -3
- realign/worker_core.py +24 -1
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Agent detail modal for the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Container, Vertical
|
|
12
|
+
from textual.screen import ModalScreen
|
|
13
|
+
from textual.widgets import DataTable, Static
|
|
14
|
+
|
|
15
|
+
from ..widgets.openable_table import OpenableDataTable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _format_dt(dt: object) -> str:
|
|
19
|
+
if isinstance(dt, datetime):
|
|
20
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
21
|
+
if dt is None:
|
|
22
|
+
return "-"
|
|
23
|
+
return str(dt)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _format_relative_time(dt: datetime) -> str:
|
|
27
|
+
now = datetime.now()
|
|
28
|
+
diff = now - dt
|
|
29
|
+
seconds = diff.total_seconds()
|
|
30
|
+
|
|
31
|
+
if seconds < 60:
|
|
32
|
+
return "just now"
|
|
33
|
+
if seconds < 3600:
|
|
34
|
+
mins = int(seconds / 60)
|
|
35
|
+
return f"{mins}m ago"
|
|
36
|
+
if seconds < 86400:
|
|
37
|
+
hours = int(seconds / 3600)
|
|
38
|
+
return f"{hours}h ago"
|
|
39
|
+
days = int(seconds / 86400)
|
|
40
|
+
return f"{days}d ago"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_iso_relative(dt: Optional[datetime]) -> str:
|
|
44
|
+
if not dt:
|
|
45
|
+
return "-"
|
|
46
|
+
try:
|
|
47
|
+
return _format_relative_time(dt)
|
|
48
|
+
except Exception:
|
|
49
|
+
return _format_dt(dt)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _shorten_id(val: str | None) -> str:
|
|
53
|
+
if not val:
|
|
54
|
+
return "-"
|
|
55
|
+
if len(val) <= 20:
|
|
56
|
+
return val
|
|
57
|
+
return f"{val[:8]}...{val[-8:]}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AgentDetailScreen(ModalScreen):
|
|
61
|
+
"""Modal that shows agent details and its sessions."""
|
|
62
|
+
|
|
63
|
+
BINDINGS = [
|
|
64
|
+
Binding("escape", "close", "Close", show=False),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
DEFAULT_CSS = """
|
|
68
|
+
AgentDetailScreen {
|
|
69
|
+
align: center middle;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
AgentDetailScreen #agent-detail-root {
|
|
73
|
+
width: 95%;
|
|
74
|
+
height: 95%;
|
|
75
|
+
padding: 1;
|
|
76
|
+
background: $background;
|
|
77
|
+
border: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
AgentDetailScreen #agent-meta {
|
|
81
|
+
height: auto;
|
|
82
|
+
margin-bottom: 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
AgentDetailScreen #agent-detail-body {
|
|
86
|
+
height: 1fr;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
AgentDetailScreen #agent-description {
|
|
90
|
+
height: auto;
|
|
91
|
+
margin-bottom: 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
AgentDetailScreen #agent-sessions-table {
|
|
95
|
+
width: 1fr;
|
|
96
|
+
height: 1fr;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
AgentDetailScreen #agent-session-preview {
|
|
100
|
+
width: 1fr;
|
|
101
|
+
height: auto;
|
|
102
|
+
margin-top: 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
AgentDetailScreen #agent-hint {
|
|
106
|
+
height: 1;
|
|
107
|
+
margin-top: 1;
|
|
108
|
+
color: $text-muted;
|
|
109
|
+
text-align: right;
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, agent_id: str) -> None:
|
|
114
|
+
super().__init__()
|
|
115
|
+
self.agent_id = agent_id
|
|
116
|
+
self._load_error: Optional[str] = None
|
|
117
|
+
self._agent_info = None
|
|
118
|
+
self._sessions: list[dict] = []
|
|
119
|
+
self._session_record_cache: dict[str, object] = {}
|
|
120
|
+
self._initialized: bool = False
|
|
121
|
+
|
|
122
|
+
def compose(self) -> ComposeResult:
|
|
123
|
+
with Container(id="agent-detail-root"):
|
|
124
|
+
yield Static(id="agent-meta")
|
|
125
|
+
with Vertical(id="agent-detail-body"):
|
|
126
|
+
yield Static(id="agent-description")
|
|
127
|
+
sessions_table = OpenableDataTable(id="agent-sessions-table")
|
|
128
|
+
sessions_table.add_columns(
|
|
129
|
+
"#",
|
|
130
|
+
"Session ID",
|
|
131
|
+
"Source",
|
|
132
|
+
"Project",
|
|
133
|
+
"Turns",
|
|
134
|
+
"Title",
|
|
135
|
+
"Last Activity",
|
|
136
|
+
)
|
|
137
|
+
sessions_table.cursor_type = "row"
|
|
138
|
+
yield sessions_table
|
|
139
|
+
yield Static(id="agent-session-preview")
|
|
140
|
+
yield Static(
|
|
141
|
+
"Click: select Enter/dblclick: open Esc: close",
|
|
142
|
+
id="agent-hint",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def on_show(self) -> None:
|
|
146
|
+
self.call_later(self._ensure_initialized)
|
|
147
|
+
|
|
148
|
+
def _ensure_initialized(self) -> None:
|
|
149
|
+
if self._initialized:
|
|
150
|
+
return
|
|
151
|
+
self._initialized = True
|
|
152
|
+
|
|
153
|
+
sessions_table = self.query_one("#agent-sessions-table", DataTable)
|
|
154
|
+
self._load_data()
|
|
155
|
+
self._update_display()
|
|
156
|
+
|
|
157
|
+
if sessions_table.row_count > 0:
|
|
158
|
+
sessions_table.focus()
|
|
159
|
+
|
|
160
|
+
def action_close(self) -> None:
|
|
161
|
+
self.app.pop_screen()
|
|
162
|
+
|
|
163
|
+
def on_openable_data_table_row_activated(
|
|
164
|
+
self, event: OpenableDataTable.RowActivated
|
|
165
|
+
) -> None:
|
|
166
|
+
if event.data_table.id != "agent-sessions-table":
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
session_id = str(event.row_key.value)
|
|
170
|
+
if not session_id:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
from .session_detail import SessionDetailScreen
|
|
174
|
+
|
|
175
|
+
self.app.push_screen(SessionDetailScreen(session_id))
|
|
176
|
+
|
|
177
|
+
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
178
|
+
if event.data_table.id != "agent-sessions-table":
|
|
179
|
+
return
|
|
180
|
+
self._update_session_preview(str(event.row_key.value))
|
|
181
|
+
|
|
182
|
+
def _load_data(self) -> None:
|
|
183
|
+
try:
|
|
184
|
+
from ...db import get_database
|
|
185
|
+
|
|
186
|
+
db = get_database()
|
|
187
|
+
self._agent_info = db.get_agent_info(self.agent_id)
|
|
188
|
+
sessions = db.get_sessions_by_agent_id(self.agent_id, limit=1000)
|
|
189
|
+
|
|
190
|
+
source_map = {
|
|
191
|
+
"claude": "Claude",
|
|
192
|
+
"codex": "Codex",
|
|
193
|
+
"gemini": "Gemini",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
self._sessions = []
|
|
197
|
+
for s in sessions:
|
|
198
|
+
session_id = str(s.id)
|
|
199
|
+
session_type = getattr(s, "session_type", None) or "unknown"
|
|
200
|
+
workspace = getattr(s, "workspace_path", None)
|
|
201
|
+
title = getattr(s, "session_title", None) or "(no title)"
|
|
202
|
+
last_activity = getattr(s, "last_activity_at", None)
|
|
203
|
+
turns = int(getattr(s, "total_turns", 0) or 0)
|
|
204
|
+
|
|
205
|
+
project = str(workspace).split("/")[-1] if workspace else "-"
|
|
206
|
+
source = source_map.get(session_type, session_type)
|
|
207
|
+
|
|
208
|
+
self._sessions.append(
|
|
209
|
+
{
|
|
210
|
+
"id": session_id,
|
|
211
|
+
"short_id": _shorten_id(session_id),
|
|
212
|
+
"source": source,
|
|
213
|
+
"project": project,
|
|
214
|
+
"turns": turns,
|
|
215
|
+
"title": title,
|
|
216
|
+
"last_activity": last_activity,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self._sessions.sort(
|
|
221
|
+
key=lambda item: item.get("last_activity") or datetime.min, reverse=True
|
|
222
|
+
)
|
|
223
|
+
self._load_error = None
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self._agent_info = None
|
|
226
|
+
self._sessions = []
|
|
227
|
+
self._session_record_cache = {}
|
|
228
|
+
self._load_error = str(e)
|
|
229
|
+
|
|
230
|
+
def _update_display(self) -> None:
|
|
231
|
+
meta = self.query_one("#agent-meta", Static)
|
|
232
|
+
description = self.query_one("#agent-description", Static)
|
|
233
|
+
preview = self.query_one("#agent-session-preview", Static)
|
|
234
|
+
|
|
235
|
+
if self._load_error:
|
|
236
|
+
meta.update(f"[red]Failed to load agent {self.agent_id}:[/red] {self._load_error}")
|
|
237
|
+
description.update("")
|
|
238
|
+
preview.update("")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
name = getattr(self._agent_info, "name", None) if self._agent_info else None
|
|
242
|
+
desc = getattr(self._agent_info, "description", None) if self._agent_info else None
|
|
243
|
+
created_at = getattr(self._agent_info, "created_at", None) if self._agent_info else None
|
|
244
|
+
updated_at = getattr(self._agent_info, "updated_at", None) if self._agent_info else None
|
|
245
|
+
|
|
246
|
+
meta_lines = [
|
|
247
|
+
f"[bold]Agent[/bold] {self.agent_id}",
|
|
248
|
+
f"[dim]Name:[/dim] {escape(name) if name else '(no name)'}",
|
|
249
|
+
f"[dim]Created:[/dim] {_format_dt(created_at)} [dim]Updated:[/dim] {_format_dt(updated_at)}",
|
|
250
|
+
f"[dim]Sessions:[/dim] {len(self._sessions)}",
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
meta.update("\n".join(meta_lines))
|
|
254
|
+
description.update(desc or "(no description)")
|
|
255
|
+
preview.update("")
|
|
256
|
+
|
|
257
|
+
table = self.query_one("#agent-sessions-table", DataTable)
|
|
258
|
+
selected_session_id: Optional[str] = None
|
|
259
|
+
try:
|
|
260
|
+
if table.row_count > 0:
|
|
261
|
+
selected_session_id = str(
|
|
262
|
+
table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
|
|
263
|
+
)
|
|
264
|
+
except Exception:
|
|
265
|
+
selected_session_id = None
|
|
266
|
+
table.clear()
|
|
267
|
+
|
|
268
|
+
for idx, s in enumerate(self._sessions, 1):
|
|
269
|
+
title_cell = s["title"]
|
|
270
|
+
if len(title_cell) > 40:
|
|
271
|
+
title_cell = title_cell[:40] + "..."
|
|
272
|
+
|
|
273
|
+
last_activity_str = _format_iso_relative(s["last_activity"])
|
|
274
|
+
|
|
275
|
+
table.add_row(
|
|
276
|
+
str(idx),
|
|
277
|
+
s["short_id"],
|
|
278
|
+
s["source"],
|
|
279
|
+
s["project"],
|
|
280
|
+
str(s["turns"]),
|
|
281
|
+
title_cell,
|
|
282
|
+
last_activity_str,
|
|
283
|
+
key=s["id"],
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if table.row_count > 0:
|
|
287
|
+
if selected_session_id:
|
|
288
|
+
try:
|
|
289
|
+
table.cursor_coordinate = (table.get_row_index(selected_session_id), 0)
|
|
290
|
+
except Exception:
|
|
291
|
+
table.cursor_coordinate = (0, 0)
|
|
292
|
+
else:
|
|
293
|
+
table.cursor_coordinate = (0, 0)
|
|
294
|
+
|
|
295
|
+
row_key = table.coordinate_to_cell_key(table.cursor_coordinate)[0]
|
|
296
|
+
self._update_session_preview(str(row_key.value))
|
|
297
|
+
|
|
298
|
+
def _get_session_record(self, session_id: str) -> Optional[object]:
|
|
299
|
+
if session_id in self._session_record_cache:
|
|
300
|
+
return self._session_record_cache[session_id]
|
|
301
|
+
try:
|
|
302
|
+
from ...db import get_database
|
|
303
|
+
|
|
304
|
+
db = get_database()
|
|
305
|
+
record = db.get_session_by_id(session_id)
|
|
306
|
+
self._session_record_cache[session_id] = record
|
|
307
|
+
return record
|
|
308
|
+
except Exception:
|
|
309
|
+
self._session_record_cache[session_id] = None
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _update_session_preview(self, session_id: str) -> None:
|
|
313
|
+
preview = self.query_one("#agent-session-preview", Static)
|
|
314
|
+
if not session_id:
|
|
315
|
+
preview.update("")
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
record = self._get_session_record(session_id)
|
|
319
|
+
if not record:
|
|
320
|
+
preview.update("[dim]No session details available.[/dim]")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
title = getattr(record, "session_title", None) or "(no title)"
|
|
324
|
+
summary = getattr(record, "session_summary", None) or "(no summary)"
|
|
325
|
+
|
|
326
|
+
preview.update(
|
|
327
|
+
"\n".join(
|
|
328
|
+
[
|
|
329
|
+
f"[bold]Title:[/bold] {escape(title)}",
|
|
330
|
+
f"[bold]Summary:[/bold] {escape(summary)}",
|
|
331
|
+
]
|
|
332
|
+
)
|
|
333
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Create agent info modal for the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.containers import Container, Horizontal
|
|
11
|
+
from textual.screen import ModalScreen
|
|
12
|
+
from textual.widgets import Button, Input, Label, Static
|
|
13
|
+
|
|
14
|
+
from ...logging_config import setup_logger
|
|
15
|
+
|
|
16
|
+
logger = setup_logger("realign.dashboard.screens.create_agent_info", "dashboard.log")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateAgentInfoScreen(ModalScreen[Optional[dict]]):
|
|
20
|
+
"""Modal to create a new agent profile.
|
|
21
|
+
|
|
22
|
+
Returns a dict with agent info on success, None on cancel.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
BINDINGS = [
|
|
26
|
+
Binding("escape", "close", "Close", show=False),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DEFAULT_CSS = """
|
|
30
|
+
CreateAgentInfoScreen {
|
|
31
|
+
align: center middle;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
CreateAgentInfoScreen #create-agent-info-root {
|
|
35
|
+
width: 60;
|
|
36
|
+
height: auto;
|
|
37
|
+
max-height: 80%;
|
|
38
|
+
padding: 1 2;
|
|
39
|
+
background: $background;
|
|
40
|
+
border: solid $accent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
CreateAgentInfoScreen #create-agent-info-title {
|
|
44
|
+
height: auto;
|
|
45
|
+
margin-bottom: 1;
|
|
46
|
+
text-style: bold;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
CreateAgentInfoScreen .section-label {
|
|
50
|
+
height: auto;
|
|
51
|
+
margin-top: 1;
|
|
52
|
+
margin-bottom: 0;
|
|
53
|
+
color: $text-muted;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
CreateAgentInfoScreen Input {
|
|
57
|
+
height: auto;
|
|
58
|
+
margin-top: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
CreateAgentInfoScreen #buttons {
|
|
62
|
+
height: auto;
|
|
63
|
+
margin-top: 2;
|
|
64
|
+
align: right middle;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
CreateAgentInfoScreen #buttons Button {
|
|
68
|
+
margin-left: 1;
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def compose(self) -> ComposeResult:
|
|
73
|
+
with Container(id="create-agent-info-root"):
|
|
74
|
+
yield Static("Create Agent Profile", id="create-agent-info-title")
|
|
75
|
+
|
|
76
|
+
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")
|
|
81
|
+
|
|
82
|
+
with Horizontal(id="buttons"):
|
|
83
|
+
yield Button("Cancel", id="cancel")
|
|
84
|
+
yield Button("Create", id="create", variant="primary")
|
|
85
|
+
|
|
86
|
+
def on_mount(self) -> None:
|
|
87
|
+
self.query_one("#agent-name", Input).focus()
|
|
88
|
+
|
|
89
|
+
def action_close(self) -> None:
|
|
90
|
+
self.dismiss(None)
|
|
91
|
+
|
|
92
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
93
|
+
button_id = event.button.id or ""
|
|
94
|
+
|
|
95
|
+
if button_id == "cancel":
|
|
96
|
+
self.dismiss(None)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if button_id == "create":
|
|
100
|
+
await self._create_agent()
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
104
|
+
"""Handle enter key in input fields."""
|
|
105
|
+
await self._create_agent()
|
|
106
|
+
|
|
107
|
+
async def _create_agent(self) -> None:
|
|
108
|
+
"""Create the agent profile."""
|
|
109
|
+
try:
|
|
110
|
+
from ...agent_names import generate_agent_name
|
|
111
|
+
from ...db import get_database
|
|
112
|
+
|
|
113
|
+
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()
|
|
118
|
+
|
|
119
|
+
agent_id = str(uuid.uuid4())
|
|
120
|
+
|
|
121
|
+
db = get_database(read_only=False)
|
|
122
|
+
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
|
+
|
|
126
|
+
self.dismiss({
|
|
127
|
+
"id": record.id,
|
|
128
|
+
"name": record.name,
|
|
129
|
+
"description": record.description or "",
|
|
130
|
+
})
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Failed to create agent: {e}")
|
|
133
|
+
self.app.notify(f"Failed to create agent: {e}", severity="error")
|
|
@@ -6,9 +6,6 @@ import contextlib
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
import io
|
|
8
8
|
import json
|
|
9
|
-
import os
|
|
10
|
-
import shutil
|
|
11
|
-
import subprocess
|
|
12
9
|
from typing import Optional
|
|
13
10
|
|
|
14
11
|
from rich.markup import escape
|
|
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
|
|
|
20
17
|
from textual.widgets import DataTable, Static, TextArea
|
|
21
18
|
|
|
22
19
|
from ..widgets.openable_table import OpenableDataTable
|
|
20
|
+
from ..clipboard import copy_text
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
def _format_dt(dt: object) -> str:
|
|
@@ -246,36 +244,17 @@ class EventDetailScreen(ModalScreen):
|
|
|
246
244
|
pass
|
|
247
245
|
|
|
248
246
|
if exit_code == 0 and share_link:
|
|
247
|
+
from ..clipboard import copy_text
|
|
248
|
+
|
|
249
249
|
if not slack_message:
|
|
250
250
|
slack_message = getattr(self._event, "slack_message", None) if self._event else None
|
|
251
251
|
|
|
252
252
|
if slack_message:
|
|
253
|
-
|
|
253
|
+
text_to_copy = str(slack_message) + "\n\n" + str(share_link)
|
|
254
254
|
else:
|
|
255
|
-
|
|
255
|
+
text_to_copy = str(share_link)
|
|
256
256
|
|
|
257
|
-
copied =
|
|
258
|
-
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
259
|
-
try:
|
|
260
|
-
copied = (
|
|
261
|
-
subprocess.run(
|
|
262
|
-
["pbcopy"],
|
|
263
|
-
input=copy_text,
|
|
264
|
-
text=True,
|
|
265
|
-
capture_output=False,
|
|
266
|
-
check=False,
|
|
267
|
-
).returncode
|
|
268
|
-
== 0
|
|
269
|
-
)
|
|
270
|
-
except Exception:
|
|
271
|
-
copied = False
|
|
272
|
-
|
|
273
|
-
if not copied:
|
|
274
|
-
try:
|
|
275
|
-
self.app.copy_to_clipboard(copy_text)
|
|
276
|
-
copied = True
|
|
277
|
-
except Exception:
|
|
278
|
-
copied = False
|
|
257
|
+
copied = copy_text(self.app, text_to_copy)
|
|
279
258
|
|
|
280
259
|
suffix = " (copied)" if copied else ""
|
|
281
260
|
self.app.notify(f"Share link: {share_link}{suffix}", title="Share", timeout=6)
|
|
@@ -228,3 +228,70 @@ TerminalPanel .context-sessions Static {
|
|
|
228
228
|
text-align: left;
|
|
229
229
|
content-align: left middle;
|
|
230
230
|
}
|
|
231
|
+
|
|
232
|
+
/* Agents tab: compact layout matching terminal panel */
|
|
233
|
+
AgentsPanel {
|
|
234
|
+
padding: 0 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
AgentsPanel:focus {
|
|
238
|
+
border: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
AgentsPanel .summary {
|
|
242
|
+
background: transparent;
|
|
243
|
+
border: none;
|
|
244
|
+
padding: 0;
|
|
245
|
+
margin: 0 0 1 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
AgentsPanel .list {
|
|
249
|
+
background: transparent;
|
|
250
|
+
border: none;
|
|
251
|
+
padding: 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
AgentsPanel Button {
|
|
255
|
+
min-width: 0;
|
|
256
|
+
background: transparent;
|
|
257
|
+
border: none;
|
|
258
|
+
padding: 0 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
AgentsPanel Button:hover {
|
|
262
|
+
background: $surface-lighten-1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
AgentsPanel .agent-row Button.agent-name {
|
|
266
|
+
text-align: left;
|
|
267
|
+
content-align: left middle;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
AgentsPanel .agent-row Button.agent-create {
|
|
271
|
+
width: auto;
|
|
272
|
+
min-width: 8;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
AgentsPanel .agent-row Button.agent-delete {
|
|
276
|
+
padding: 0;
|
|
277
|
+
width: 3;
|
|
278
|
+
min-width: 3;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
AgentsPanel .terminal-list {
|
|
282
|
+
background: transparent;
|
|
283
|
+
border: none;
|
|
284
|
+
padding: 0;
|
|
285
|
+
margin: 0 0 1 2;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
AgentsPanel .terminal-row Button.terminal-switch {
|
|
289
|
+
text-align: left;
|
|
290
|
+
content-align: left top;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
AgentsPanel .terminal-row Button.terminal-close {
|
|
294
|
+
padding: 0;
|
|
295
|
+
width: 3;
|
|
296
|
+
min-width: 3;
|
|
297
|
+
}
|
|
@@ -9,6 +9,7 @@ from .config_panel import ConfigPanel
|
|
|
9
9
|
from .search_panel import SearchPanel
|
|
10
10
|
from .openable_table import OpenableDataTable
|
|
11
11
|
from .terminal_panel import TerminalPanel
|
|
12
|
+
from .agents_panel import AgentsPanel
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"AlineHeader",
|
|
@@ -20,4 +21,5 @@ __all__ = [
|
|
|
20
21
|
"SearchPanel",
|
|
21
22
|
"OpenableDataTable",
|
|
22
23
|
"TerminalPanel",
|
|
24
|
+
"AgentsPanel",
|
|
23
25
|
]
|