aline-ai 0.5.5__py3-none-any.whl → 0.5.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.
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/METADATA +1 -1
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/RECORD +18 -16
- realign/__init__.py +1 -1
- realign/adapters/claude.py +13 -7
- realign/cli.py +16 -4
- realign/commands/init.py +31 -5
- realign/dashboard/app.py +32 -22
- realign/dashboard/screens/__init__.py +10 -1
- realign/dashboard/screens/create_agent.py +244 -0
- realign/dashboard/screens/help_screen.py +114 -0
- realign/dashboard/widgets/events_table.py +311 -69
- realign/dashboard/widgets/header.py +1 -1
- realign/dashboard/widgets/sessions_table.py +380 -70
- realign/dashboard/widgets/terminal_panel.py +132 -196
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Help screen modal for the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import Container, Vertical
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HelpScreen(ModalScreen):
|
|
13
|
+
"""Modal showing keyboard shortcuts and help information."""
|
|
14
|
+
|
|
15
|
+
BINDINGS = [
|
|
16
|
+
Binding("escape", "close", "Close", show=False),
|
|
17
|
+
Binding("?", "close", "Close", show=False),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
DEFAULT_CSS = """
|
|
21
|
+
HelpScreen {
|
|
22
|
+
align: center middle;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
HelpScreen #help-root {
|
|
26
|
+
width: 50;
|
|
27
|
+
height: auto;
|
|
28
|
+
max-height: 80%;
|
|
29
|
+
padding: 1 2;
|
|
30
|
+
background: $background;
|
|
31
|
+
border: solid $accent;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
HelpScreen #help-title {
|
|
35
|
+
height: auto;
|
|
36
|
+
margin-bottom: 1;
|
|
37
|
+
text-style: bold;
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
HelpScreen .shortcut-section {
|
|
42
|
+
height: auto;
|
|
43
|
+
margin-bottom: 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
HelpScreen .section-title {
|
|
47
|
+
height: auto;
|
|
48
|
+
color: $text-muted;
|
|
49
|
+
margin-bottom: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
HelpScreen .shortcut-row {
|
|
53
|
+
height: auto;
|
|
54
|
+
padding: 0 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
HelpScreen .shortcut-key {
|
|
58
|
+
width: 14;
|
|
59
|
+
height: auto;
|
|
60
|
+
color: $accent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
HelpScreen .shortcut-desc {
|
|
64
|
+
width: 1fr;
|
|
65
|
+
height: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
HelpScreen #close-btn {
|
|
69
|
+
margin-top: 1;
|
|
70
|
+
width: 100%;
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def compose(self) -> ComposeResult:
|
|
75
|
+
with Container(id="help-root"):
|
|
76
|
+
yield Static("Keyboard Shortcuts", id="help-title")
|
|
77
|
+
|
|
78
|
+
with Vertical(classes="shortcut-section"):
|
|
79
|
+
yield Static("Navigation", classes="section-title")
|
|
80
|
+
yield self._shortcut_row("Tab", "Next tab")
|
|
81
|
+
yield self._shortcut_row("Shift+Tab", "Previous tab")
|
|
82
|
+
yield self._shortcut_row("n", "Next page")
|
|
83
|
+
yield self._shortcut_row("p", "Previous page")
|
|
84
|
+
|
|
85
|
+
with Vertical(classes="shortcut-section"):
|
|
86
|
+
yield Static("Actions", classes="section-title")
|
|
87
|
+
yield self._shortcut_row("Enter", "Open selected item")
|
|
88
|
+
yield self._shortcut_row("Space", "Toggle selection")
|
|
89
|
+
yield self._shortcut_row("c", "Create event")
|
|
90
|
+
yield self._shortcut_row("l", "Load context")
|
|
91
|
+
yield self._shortcut_row("y", "Import share")
|
|
92
|
+
yield self._shortcut_row("s", "Switch view")
|
|
93
|
+
yield self._shortcut_row("r", "Refresh")
|
|
94
|
+
|
|
95
|
+
with Vertical(classes="shortcut-section"):
|
|
96
|
+
yield Static("General", classes="section-title")
|
|
97
|
+
yield self._shortcut_row("?", "Show this help")
|
|
98
|
+
yield self._shortcut_row("Ctrl+C x2", "Quit")
|
|
99
|
+
|
|
100
|
+
yield Button("Close", id="close-btn", variant="primary")
|
|
101
|
+
|
|
102
|
+
def _shortcut_row(self, key: str, description: str) -> Static:
|
|
103
|
+
"""Create a shortcut row with key and description."""
|
|
104
|
+
return Static(f"[bold $accent]{key:<12}[/] {description}", classes="shortcut-row")
|
|
105
|
+
|
|
106
|
+
def on_mount(self) -> None:
|
|
107
|
+
self.query_one("#close-btn", Button).focus()
|
|
108
|
+
|
|
109
|
+
def action_close(self) -> None:
|
|
110
|
+
self.app.pop_screen()
|
|
111
|
+
|
|
112
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
113
|
+
if event.button.id == "close-btn":
|
|
114
|
+
self.app.pop_screen()
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""Events Table Widget with keyboard pagination."""
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
3
9
|
from datetime import datetime
|
|
4
10
|
from typing import Optional, Set
|
|
5
11
|
from urllib.parse import urlparse
|
|
@@ -7,7 +13,7 @@ from urllib.parse import urlparse
|
|
|
7
13
|
from textual import events
|
|
8
14
|
from textual.app import ComposeResult
|
|
9
15
|
from textual.binding import Binding
|
|
10
|
-
from textual.containers import Container, Horizontal
|
|
16
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
11
17
|
from textual.reactive import reactive
|
|
12
18
|
from textual.worker import Worker, WorkerState
|
|
13
19
|
from textual.widgets import Button, DataTable, Static
|
|
@@ -44,7 +50,7 @@ class EventsListTable(OpenableDataTable):
|
|
|
44
50
|
self.owner.apply_mouse_selection(row_index, shift=event.shift, meta=event.meta)
|
|
45
51
|
|
|
46
52
|
|
|
47
|
-
class EventsTable(Container
|
|
53
|
+
class EventsTable(Container):
|
|
48
54
|
"""Table displaying events with keyboard pagination support."""
|
|
49
55
|
|
|
50
56
|
DEFAULT_CSS = """
|
|
@@ -54,26 +60,14 @@ class EventsTable(Container, can_focus=True):
|
|
|
54
60
|
overflow: hidden;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
EventsTable
|
|
58
|
-
border: solid $accent;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
EventsTable .summary-section {
|
|
63
|
+
EventsTable .action-section {
|
|
62
64
|
height: auto;
|
|
63
65
|
margin-bottom: 1;
|
|
64
|
-
padding: 1;
|
|
65
|
-
background: $surface-darken-1;
|
|
66
|
-
border: solid $primary-darken-2;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
EventsTable .summary-section Static {
|
|
70
|
-
width: 1fr;
|
|
71
|
-
height: auto;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
EventsTable .
|
|
75
|
-
width:
|
|
76
|
-
margin-
|
|
68
|
+
EventsTable .action-section Button {
|
|
69
|
+
width: auto;
|
|
70
|
+
margin-bottom: 1;
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
EventsTable .section-header {
|
|
@@ -83,13 +77,15 @@ class EventsTable(Container, can_focus=True):
|
|
|
83
77
|
|
|
84
78
|
EventsTable .table-container {
|
|
85
79
|
height: auto;
|
|
86
|
-
overflow:
|
|
80
|
+
overflow-x: auto;
|
|
81
|
+
overflow-y: hidden;
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
EventsTable DataTable {
|
|
90
85
|
height: auto;
|
|
91
86
|
max-height: 100%;
|
|
92
|
-
overflow:
|
|
87
|
+
overflow-x: auto;
|
|
88
|
+
overflow-y: hidden;
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
EventsTable .pagination-info {
|
|
@@ -98,6 +94,12 @@ class EventsTable(Container, can_focus=True):
|
|
|
98
94
|
color: $text-muted;
|
|
99
95
|
text-align: center;
|
|
100
96
|
}
|
|
97
|
+
|
|
98
|
+
EventsTable .stats-info {
|
|
99
|
+
height: 1;
|
|
100
|
+
color: $text-muted;
|
|
101
|
+
text-align: center;
|
|
102
|
+
}
|
|
101
103
|
"""
|
|
102
104
|
|
|
103
105
|
# Reactive properties
|
|
@@ -108,25 +110,42 @@ class EventsTable(Container, can_focus=True):
|
|
|
108
110
|
def __init__(self) -> None:
|
|
109
111
|
super().__init__()
|
|
110
112
|
self._events: list = []
|
|
113
|
+
self._events_by_id: dict = {} # Index events by id for quick lookup
|
|
111
114
|
self._total_events: int = 0
|
|
112
115
|
self._selected_event_ids: Set[str] = set()
|
|
113
116
|
self._selection_anchor_row: Optional[int] = None
|
|
114
117
|
self._last_wrap_mode: bool = bool(self.wrap_mode)
|
|
115
118
|
self._refresh_worker: Optional[Worker] = None
|
|
119
|
+
self._share_export_worker: Optional[Worker] = None
|
|
116
120
|
self._refresh_timer = None
|
|
117
121
|
self._active_refresh_snapshot: Optional[tuple[int, int]] = None
|
|
118
122
|
self._pending_refresh_snapshot: Optional[tuple[int, int]] = None
|
|
119
123
|
|
|
120
124
|
def compose(self) -> ComposeResult:
|
|
121
125
|
"""Compose the events table layout."""
|
|
122
|
-
with
|
|
123
|
-
yield
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
with Vertical(classes="action-section"):
|
|
127
|
+
yield Button(
|
|
128
|
+
"Load selected context to current agent",
|
|
129
|
+
id="load-context-btn",
|
|
130
|
+
variant="primary",
|
|
131
|
+
disabled=True,
|
|
132
|
+
)
|
|
133
|
+
yield Button(
|
|
134
|
+
"Share selected events to others",
|
|
135
|
+
id="share-event-btn",
|
|
136
|
+
variant="primary",
|
|
137
|
+
disabled=True,
|
|
138
|
+
)
|
|
139
|
+
yield Button(
|
|
140
|
+
"Import context from others",
|
|
141
|
+
id="share-import-btn",
|
|
142
|
+
variant="primary",
|
|
143
|
+
)
|
|
126
144
|
yield Static(id="section-header", classes="section-header")
|
|
127
145
|
with Container(classes="table-container"):
|
|
128
146
|
yield EventsListTable(id="events-table")
|
|
129
147
|
yield Static(id="pagination-info", classes="pagination-info")
|
|
148
|
+
yield Static(id="stats-info", classes="stats-info")
|
|
130
149
|
|
|
131
150
|
def on_mount(self) -> None:
|
|
132
151
|
"""Set up the table on mount."""
|
|
@@ -170,7 +189,9 @@ class EventsTable(Container, can_focus=True):
|
|
|
170
189
|
except Exception:
|
|
171
190
|
pass
|
|
172
191
|
|
|
173
|
-
def on_openable_data_table_row_activated(
|
|
192
|
+
def on_openable_data_table_row_activated(
|
|
193
|
+
self, event: OpenableDataTable.RowActivated
|
|
194
|
+
) -> None:
|
|
174
195
|
if event.data_table.id != "events-table":
|
|
175
196
|
return
|
|
176
197
|
|
|
@@ -190,7 +211,13 @@ class EventsTable(Container, can_focus=True):
|
|
|
190
211
|
def _setup_table_columns(self, table: DataTable) -> None:
|
|
191
212
|
table.clear(columns=True)
|
|
192
213
|
table.add_column("✓", key="sel", width=2)
|
|
193
|
-
table.
|
|
214
|
+
table.add_column("#", key="index", width=4)
|
|
215
|
+
table.add_column("Title", key="title") # Auto width for full title
|
|
216
|
+
table.add_column("Share", key="share", width=12)
|
|
217
|
+
table.add_column("Type", key="type", width=8)
|
|
218
|
+
table.add_column("Sessions", key="sessions", width=8)
|
|
219
|
+
table.add_column("Event ID", key="event_id", width=12)
|
|
220
|
+
table.add_column("Created", key="created", width=10)
|
|
194
221
|
table.cursor_type = "row"
|
|
195
222
|
|
|
196
223
|
def get_selected_event_ids(self) -> list[str]:
|
|
@@ -206,7 +233,9 @@ class EventsTable(Container, can_focus=True):
|
|
|
206
233
|
if table.row_count == 0:
|
|
207
234
|
return
|
|
208
235
|
try:
|
|
209
|
-
event_id = str(
|
|
236
|
+
event_id = str(
|
|
237
|
+
table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
|
|
238
|
+
)
|
|
210
239
|
except Exception:
|
|
211
240
|
return
|
|
212
241
|
|
|
@@ -255,19 +284,28 @@ class EventsTable(Container, can_focus=True):
|
|
|
255
284
|
self._selected_event_ids.update(ids_in_range)
|
|
256
285
|
else:
|
|
257
286
|
self._selected_event_ids = set(ids_in_range)
|
|
258
|
-
|
|
287
|
+
else:
|
|
288
|
+
# Toggle selection on click (no modifier keys needed)
|
|
259
289
|
if clicked_id in self._selected_event_ids:
|
|
260
290
|
self._selected_event_ids.remove(clicked_id)
|
|
261
291
|
else:
|
|
262
292
|
self._selected_event_ids.add(clicked_id)
|
|
263
|
-
else:
|
|
264
|
-
self._selected_event_ids = {clicked_id}
|
|
265
293
|
|
|
266
294
|
self._selection_anchor_row = row_index
|
|
267
295
|
self._refresh_checkboxes_only()
|
|
268
296
|
|
|
269
297
|
def _checkbox_cell(self, event_id: str) -> str:
|
|
270
|
-
return
|
|
298
|
+
return (
|
|
299
|
+
"[bold green]●[/bold green]"
|
|
300
|
+
if event_id in self._selected_event_ids
|
|
301
|
+
else "○"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _format_cell(self, value: str, event_id: str) -> str:
|
|
305
|
+
"""Format cell value with bold if selected."""
|
|
306
|
+
if event_id in self._selected_event_ids:
|
|
307
|
+
return f"[bold]{value}[/bold]"
|
|
308
|
+
return value
|
|
271
309
|
|
|
272
310
|
def _refresh_checkboxes_only(self) -> None:
|
|
273
311
|
table = self.query_one("#events-table", DataTable)
|
|
@@ -282,20 +320,49 @@ class EventsTable(Container, can_focus=True):
|
|
|
282
320
|
continue
|
|
283
321
|
if not eid:
|
|
284
322
|
continue
|
|
323
|
+
event = self._events_by_id.get(eid)
|
|
324
|
+
if not event:
|
|
325
|
+
continue
|
|
285
326
|
try:
|
|
327
|
+
# Update all cells in the row with proper formatting
|
|
328
|
+
share_id = event.get("share_id") or "-"
|
|
286
329
|
table.update_cell(eid, "sel", self._checkbox_cell(eid))
|
|
330
|
+
table.update_cell(
|
|
331
|
+
eid, "index", self._format_cell(str(event["index"]), eid)
|
|
332
|
+
)
|
|
333
|
+
table.update_cell(eid, "title", self._format_cell(event["title"], eid))
|
|
334
|
+
table.update_cell(eid, "share", self._format_cell(share_id, eid))
|
|
335
|
+
table.update_cell(eid, "type", self._format_cell(event["type"], eid))
|
|
336
|
+
table.update_cell(
|
|
337
|
+
eid, "sessions", self._format_cell(str(event["sessions"]), eid)
|
|
338
|
+
)
|
|
339
|
+
table.update_cell(
|
|
340
|
+
eid, "event_id", self._format_cell(event["short_id"], eid)
|
|
341
|
+
)
|
|
342
|
+
table.update_cell(
|
|
343
|
+
eid, "created", self._format_cell(event["created"], eid)
|
|
344
|
+
)
|
|
287
345
|
except Exception:
|
|
288
346
|
continue
|
|
289
347
|
|
|
290
348
|
self._update_summary_widget()
|
|
291
349
|
|
|
292
350
|
def _update_summary_widget(self) -> None:
|
|
293
|
-
|
|
294
|
-
|
|
351
|
+
# Update stats at the bottom
|
|
352
|
+
stats_widget = self.query_one("#stats-info", Static)
|
|
295
353
|
selected_count = len(self._selected_event_ids)
|
|
354
|
+
|
|
355
|
+
stats_parts = [f"Total: {self._total_events}"]
|
|
296
356
|
if selected_count:
|
|
297
|
-
|
|
298
|
-
|
|
357
|
+
stats_parts.append(f"Selected: {selected_count}")
|
|
358
|
+
|
|
359
|
+
stats_widget.update(f"[dim]{' | '.join(stats_parts)}[/dim]")
|
|
360
|
+
|
|
361
|
+
# Enable/disable buttons based on selection
|
|
362
|
+
load_btn = self.query_one("#load-context-btn", Button)
|
|
363
|
+
load_btn.disabled = selected_count == 0
|
|
364
|
+
share_btn = self.query_one("#share-event-btn", Button)
|
|
365
|
+
share_btn.disabled = selected_count == 0
|
|
299
366
|
|
|
300
367
|
def _sync_to_available_height(self) -> None:
|
|
301
368
|
"""Recalculate rows per page and reload if the page size changed."""
|
|
@@ -342,6 +409,9 @@ class EventsTable(Container, can_focus=True):
|
|
|
342
409
|
if callable(action):
|
|
343
410
|
await action()
|
|
344
411
|
return
|
|
412
|
+
if button_id == "share-event-btn":
|
|
413
|
+
self._start_share_export()
|
|
414
|
+
return
|
|
345
415
|
|
|
346
416
|
def action_share_import(self) -> None:
|
|
347
417
|
from ..screens import ShareImportScreen
|
|
@@ -402,6 +472,15 @@ class EventsTable(Container, can_focus=True):
|
|
|
402
472
|
self.refresh_data()
|
|
403
473
|
|
|
404
474
|
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
475
|
+
# Handle share export worker
|
|
476
|
+
if (
|
|
477
|
+
self._share_export_worker is not None
|
|
478
|
+
and event.worker is self._share_export_worker
|
|
479
|
+
):
|
|
480
|
+
self._on_share_export_worker_changed(event)
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
# Handle refresh worker
|
|
405
484
|
if self._refresh_worker is None or event.worker is not self._refresh_worker:
|
|
406
485
|
return
|
|
407
486
|
|
|
@@ -424,13 +503,182 @@ class EventsTable(Container, can_focus=True):
|
|
|
424
503
|
self._total_events = 0
|
|
425
504
|
try:
|
|
426
505
|
self._events = list(result.get("events") or [])
|
|
506
|
+
self._events_by_id = {e["id"]: e for e in self._events}
|
|
427
507
|
except Exception:
|
|
428
508
|
self._events = []
|
|
509
|
+
self._events_by_id = {}
|
|
429
510
|
self._update_display()
|
|
430
511
|
|
|
431
512
|
if self._pending_refresh_snapshot is not None:
|
|
432
513
|
self.refresh_data()
|
|
433
514
|
|
|
515
|
+
def _start_share_export(self) -> None:
|
|
516
|
+
"""Export selected events as share links."""
|
|
517
|
+
selected_ids = list(self._selected_event_ids)
|
|
518
|
+
if not selected_ids:
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
if (
|
|
522
|
+
self._share_export_worker is not None
|
|
523
|
+
and self._share_export_worker.state
|
|
524
|
+
in (
|
|
525
|
+
WorkerState.PENDING,
|
|
526
|
+
WorkerState.RUNNING,
|
|
527
|
+
)
|
|
528
|
+
):
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
def work() -> dict:
|
|
532
|
+
# Export the first selected event (for now, support single event)
|
|
533
|
+
event_id = selected_ids[0]
|
|
534
|
+
|
|
535
|
+
stdout_exp = io.StringIO()
|
|
536
|
+
stderr_exp = io.StringIO()
|
|
537
|
+
|
|
538
|
+
with (
|
|
539
|
+
contextlib.redirect_stdout(stdout_exp),
|
|
540
|
+
contextlib.redirect_stderr(stderr_exp),
|
|
541
|
+
):
|
|
542
|
+
from ...commands import export_shares
|
|
543
|
+
|
|
544
|
+
exp_exit_code = export_shares.export_shares_interactive_command(
|
|
545
|
+
indices=event_id,
|
|
546
|
+
password=None,
|
|
547
|
+
enable_preview=False,
|
|
548
|
+
json_output=True,
|
|
549
|
+
compact=True,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
exp_output = stdout_exp.getvalue().strip()
|
|
553
|
+
result: dict = {
|
|
554
|
+
"exit_code": exp_exit_code,
|
|
555
|
+
"output": exp_output,
|
|
556
|
+
"stderr": stderr_exp.getvalue().strip(),
|
|
557
|
+
"event_id": event_id,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if exp_output:
|
|
561
|
+
# Try to extract JSON from output (may contain other text)
|
|
562
|
+
try:
|
|
563
|
+
result["json"] = json.loads(exp_output)
|
|
564
|
+
except Exception:
|
|
565
|
+
# Try to find JSON object in output
|
|
566
|
+
json_start = exp_output.find("{")
|
|
567
|
+
json_end = exp_output.rfind("}") + 1
|
|
568
|
+
if json_start >= 0 and json_end > json_start:
|
|
569
|
+
try:
|
|
570
|
+
result["json"] = json.loads(exp_output[json_start:json_end])
|
|
571
|
+
except Exception:
|
|
572
|
+
result["json"] = None
|
|
573
|
+
else:
|
|
574
|
+
result["json"] = None
|
|
575
|
+
else:
|
|
576
|
+
result["json"] = None
|
|
577
|
+
|
|
578
|
+
return result
|
|
579
|
+
|
|
580
|
+
self.app.notify("Creating share link...", title="Share", timeout=2)
|
|
581
|
+
self._share_export_worker = self.run_worker(
|
|
582
|
+
work, thread=True, exit_on_error=False
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
def _on_share_export_worker_changed(self, event: Worker.StateChanged) -> None:
|
|
586
|
+
"""Handle share export worker state changes."""
|
|
587
|
+
if event.state == WorkerState.ERROR:
|
|
588
|
+
err = (
|
|
589
|
+
self._share_export_worker.error
|
|
590
|
+
if self._share_export_worker
|
|
591
|
+
else "Unknown error"
|
|
592
|
+
)
|
|
593
|
+
self.app.notify(
|
|
594
|
+
f"Share export failed: {err}", title="Share", severity="error"
|
|
595
|
+
)
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
if event.state != WorkerState.SUCCESS:
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
result = self._share_export_worker.result if self._share_export_worker else {}
|
|
602
|
+
if not result:
|
|
603
|
+
result = {}
|
|
604
|
+
|
|
605
|
+
exit_code = int(result.get("exit_code", 1))
|
|
606
|
+
|
|
607
|
+
if exit_code != 0:
|
|
608
|
+
stderr = result.get("stderr", "")
|
|
609
|
+
self.app.notify(
|
|
610
|
+
f"Share export failed: {stderr}" if stderr else "Share export failed",
|
|
611
|
+
title="Share",
|
|
612
|
+
severity="error",
|
|
613
|
+
)
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
payload = result.get("json") or {}
|
|
617
|
+
share_link = payload.get("share_link") or payload.get("share_url")
|
|
618
|
+
slack_message = (
|
|
619
|
+
payload.get("slack_message") if isinstance(payload, dict) else None
|
|
620
|
+
)
|
|
621
|
+
event_id = result.get("event_id")
|
|
622
|
+
|
|
623
|
+
# Try to fetch share_link and slack_message from database
|
|
624
|
+
if event_id:
|
|
625
|
+
try:
|
|
626
|
+
from ...db import get_database
|
|
627
|
+
|
|
628
|
+
db = get_database()
|
|
629
|
+
db_event = db.get_event_by_id(event_id)
|
|
630
|
+
if db_event:
|
|
631
|
+
if not share_link:
|
|
632
|
+
share_link = getattr(db_event, "share_url", None)
|
|
633
|
+
if not slack_message:
|
|
634
|
+
slack_message = getattr(db_event, "slack_message", None)
|
|
635
|
+
except Exception:
|
|
636
|
+
pass
|
|
637
|
+
|
|
638
|
+
if not share_link:
|
|
639
|
+
self.app.notify(
|
|
640
|
+
"Share export completed but no link generated",
|
|
641
|
+
title="Share",
|
|
642
|
+
severity="warning",
|
|
643
|
+
)
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
# Build copy text
|
|
647
|
+
if slack_message:
|
|
648
|
+
copy_text = str(slack_message) + "\n\n" + str(share_link)
|
|
649
|
+
else:
|
|
650
|
+
copy_text = str(share_link)
|
|
651
|
+
|
|
652
|
+
# Copy to clipboard
|
|
653
|
+
copied = False
|
|
654
|
+
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
655
|
+
try:
|
|
656
|
+
copied = (
|
|
657
|
+
subprocess.run(
|
|
658
|
+
["pbcopy"],
|
|
659
|
+
input=copy_text,
|
|
660
|
+
text=True,
|
|
661
|
+
capture_output=False,
|
|
662
|
+
check=False,
|
|
663
|
+
).returncode
|
|
664
|
+
== 0
|
|
665
|
+
)
|
|
666
|
+
except Exception:
|
|
667
|
+
copied = False
|
|
668
|
+
|
|
669
|
+
if not copied:
|
|
670
|
+
try:
|
|
671
|
+
self.app.copy_to_clipboard(copy_text)
|
|
672
|
+
copied = True
|
|
673
|
+
except Exception:
|
|
674
|
+
copied = False
|
|
675
|
+
|
|
676
|
+
suffix = " (copied to clipboard)" if copied else ""
|
|
677
|
+
self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
|
|
678
|
+
|
|
679
|
+
# Refresh to show updated share info
|
|
680
|
+
self.refresh_data()
|
|
681
|
+
|
|
434
682
|
def _extract_share_id(self, share_url: Optional[str]) -> str:
|
|
435
683
|
if not share_url:
|
|
436
684
|
return ""
|
|
@@ -555,8 +803,7 @@ class EventsTable(Container, can_focus=True):
|
|
|
555
803
|
|
|
556
804
|
# Update section header
|
|
557
805
|
header_widget = self.query_one("#section-header", Static)
|
|
558
|
-
|
|
559
|
-
header_widget.update(f"[bold]Events[/bold] [dim]({mode})[/dim]")
|
|
806
|
+
header_widget.update("[bold]Events[/bold]")
|
|
560
807
|
|
|
561
808
|
# Update table
|
|
562
809
|
table = self.query_one("#events-table", DataTable)
|
|
@@ -568,49 +815,44 @@ class EventsTable(Container, can_focus=True):
|
|
|
568
815
|
)
|
|
569
816
|
except Exception:
|
|
570
817
|
selected_event_id = None
|
|
571
|
-
|
|
572
|
-
self._setup_table_columns(table)
|
|
573
|
-
self._last_wrap_mode = bool(self.wrap_mode)
|
|
574
|
-
else:
|
|
575
|
-
table.clear()
|
|
818
|
+
table.clear()
|
|
576
819
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
else:
|
|
581
|
-
table.styles.overflow_x = "hidden"
|
|
582
|
-
table.show_horizontal_scrollbar = False
|
|
820
|
+
# Always enable horizontal scrollbar
|
|
821
|
+
table.styles.overflow_x = "auto"
|
|
822
|
+
table.show_horizontal_scrollbar = True
|
|
583
823
|
|
|
584
824
|
for event in self._events:
|
|
825
|
+
eid = event["id"]
|
|
826
|
+
share_id = event.get("share_id") or "-"
|
|
585
827
|
title = event["title"]
|
|
586
|
-
if self.wrap_mode:
|
|
587
|
-
title_cell = title
|
|
588
|
-
else:
|
|
589
|
-
title_cell = title[:50] + "..." if len(title) > 50 else title
|
|
590
828
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
829
|
+
if not self.wrap_mode and len(title) > 60:
|
|
830
|
+
title = title[:57] + "..."
|
|
831
|
+
|
|
832
|
+
share_val = share_id
|
|
833
|
+
if self.wrap_mode and event.get("share_url"):
|
|
834
|
+
share_val = event.get("share_url")
|
|
597
835
|
|
|
836
|
+
# Column order: ✓, #, Title, Share, Type, Sessions, Event ID, Created
|
|
598
837
|
table.add_row(
|
|
599
|
-
self._checkbox_cell(
|
|
600
|
-
str(event["index"]),
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
event["type"],
|
|
604
|
-
str(event["sessions"]),
|
|
605
|
-
|
|
606
|
-
event["created"],
|
|
607
|
-
key=
|
|
838
|
+
self._checkbox_cell(eid),
|
|
839
|
+
self._format_cell(str(event["index"]), eid),
|
|
840
|
+
self._format_cell(title, eid),
|
|
841
|
+
self._format_cell(share_val, eid),
|
|
842
|
+
self._format_cell(event["type"], eid),
|
|
843
|
+
self._format_cell(str(event["sessions"]), eid),
|
|
844
|
+
self._format_cell(event["short_id"], eid),
|
|
845
|
+
self._format_cell(event["created"], eid),
|
|
846
|
+
key=eid,
|
|
608
847
|
)
|
|
609
848
|
|
|
610
849
|
if table.row_count > 0:
|
|
611
850
|
if selected_event_id:
|
|
612
851
|
try:
|
|
613
|
-
table.cursor_coordinate = (
|
|
852
|
+
table.cursor_coordinate = (
|
|
853
|
+
table.get_row_index(selected_event_id),
|
|
854
|
+
0,
|
|
855
|
+
)
|
|
614
856
|
except Exception:
|
|
615
857
|
table.cursor_coordinate = (0, 0)
|
|
616
858
|
else:
|
|
@@ -620,7 +862,7 @@ class EventsTable(Container, can_focus=True):
|
|
|
620
862
|
total_pages = self._get_total_pages()
|
|
621
863
|
pagination_widget = self.query_one("#pagination-info", Static)
|
|
622
864
|
pagination_widget.update(
|
|
623
|
-
f"[dim]Page {self.current_page}/{total_pages} ({self._total_events} total) │ (p) prev (n) next
|
|
865
|
+
f"[dim]Page {self.current_page}/{total_pages} ({self._total_events} total) │ (p) prev (n) next[/dim]"
|
|
624
866
|
)
|
|
625
867
|
|
|
626
868
|
def _shorten_id(self, event_id: str) -> str:
|
|
@@ -40,5 +40,5 @@ class AlineHeader(Static):
|
|
|
40
40
|
line3 = " ███████║██║ ██║██╔██╗ ██║█████╗ "
|
|
41
41
|
line4 = " ██╔══██║██║ ██║██║╚██╗██║██╔══╝ "
|
|
42
42
|
line5 = " ██║ ██║███████╗██║██║ ╚████║███████╗"
|
|
43
|
-
info = f"[dim]v{self._version} │ Shared
|
|
43
|
+
info = f"[dim]v{self._version} │ Shared Agent Context[/dim]"
|
|
44
44
|
return f"{line1}\n{line2}\n{line3}\n{line4}\n{line5} {info}"
|