aline-ai 0.5.4__py3-none-any.whl → 0.5.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.5.4.dist-info → aline_ai-0.5.6.dist-info}/METADATA +1 -1
- aline_ai-0.5.6.dist-info/RECORD +95 -0
- realign/__init__.py +1 -1
- realign/adapters/antigravity.py +28 -20
- realign/adapters/base.py +46 -50
- realign/adapters/claude.py +14 -14
- realign/adapters/codex.py +7 -7
- realign/adapters/gemini.py +11 -11
- realign/adapters/registry.py +14 -10
- realign/claude_detector.py +2 -2
- realign/claude_hooks/__init__.py +3 -3
- realign/claude_hooks/permission_request_hook_installer.py +31 -32
- realign/claude_hooks/stop_hook.py +4 -1
- realign/claude_hooks/stop_hook_installer.py +30 -31
- realign/cli.py +23 -4
- realign/codex_detector.py +11 -11
- realign/commands/add.py +88 -65
- realign/commands/config.py +3 -12
- realign/commands/context.py +3 -1
- realign/commands/export_shares.py +86 -127
- realign/commands/import_shares.py +145 -155
- realign/commands/init.py +166 -30
- realign/commands/restore.py +18 -6
- realign/commands/search.py +14 -42
- realign/commands/upgrade.py +155 -11
- realign/commands/watcher.py +98 -219
- realign/commands/worker.py +29 -6
- realign/config.py +25 -20
- realign/context.py +1 -3
- realign/dashboard/app.py +34 -24
- realign/dashboard/screens/__init__.py +10 -1
- realign/dashboard/screens/create_agent.py +244 -0
- realign/dashboard/screens/create_event.py +3 -1
- realign/dashboard/screens/event_detail.py +14 -6
- realign/dashboard/screens/help_screen.py +114 -0
- realign/dashboard/screens/session_detail.py +3 -1
- realign/dashboard/screens/share_import.py +7 -3
- realign/dashboard/tmux_manager.py +54 -9
- realign/dashboard/widgets/config_panel.py +85 -1
- realign/dashboard/widgets/events_table.py +314 -70
- realign/dashboard/widgets/header.py +2 -1
- realign/dashboard/widgets/search_panel.py +37 -27
- realign/dashboard/widgets/sessions_table.py +404 -85
- realign/dashboard/widgets/terminal_panel.py +155 -175
- realign/dashboard/widgets/watcher_panel.py +6 -2
- realign/dashboard/widgets/worker_panel.py +10 -1
- realign/db/__init__.py +1 -1
- realign/db/base.py +5 -15
- realign/db/locks.py +0 -1
- realign/db/migration.py +82 -76
- realign/db/schema.py +2 -6
- realign/db/sqlite_db.py +23 -41
- realign/events/__init__.py +0 -1
- realign/events/event_summarizer.py +27 -15
- realign/events/session_summarizer.py +29 -15
- realign/file_lock.py +1 -0
- realign/hooks.py +150 -60
- realign/logging_config.py +12 -15
- realign/mcp_server.py +30 -51
- realign/mcp_watcher.py +0 -1
- realign/models/event.py +29 -20
- realign/prompts/__init__.py +7 -7
- realign/prompts/presets.py +15 -11
- realign/redactor.py +99 -59
- realign/triggers/__init__.py +9 -9
- realign/triggers/antigravity_trigger.py +30 -28
- realign/triggers/base.py +4 -3
- realign/triggers/claude_trigger.py +104 -85
- realign/triggers/codex_trigger.py +15 -5
- realign/triggers/gemini_trigger.py +57 -47
- realign/triggers/next_turn_trigger.py +3 -1
- realign/triggers/registry.py +6 -2
- realign/triggers/turn_status.py +3 -1
- realign/watcher_core.py +306 -131
- realign/watcher_daemon.py +8 -8
- realign/worker_core.py +3 -1
- realign/worker_daemon.py +3 -1
- aline_ai-0.5.4.dist-info/RECORD +0 -93
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/top_level.txt +0 -0
|
@@ -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,60 +803,56 @@ 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)
|
|
563
810
|
selected_event_id = None
|
|
564
811
|
try:
|
|
565
812
|
if table.row_count > 0:
|
|
566
|
-
selected_event_id = str(
|
|
813
|
+
selected_event_id = str(
|
|
814
|
+
table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
|
|
815
|
+
)
|
|
567
816
|
except Exception:
|
|
568
817
|
selected_event_id = None
|
|
569
|
-
|
|
570
|
-
self._setup_table_columns(table)
|
|
571
|
-
self._last_wrap_mode = bool(self.wrap_mode)
|
|
572
|
-
else:
|
|
573
|
-
table.clear()
|
|
818
|
+
table.clear()
|
|
574
819
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
else:
|
|
579
|
-
table.styles.overflow_x = "hidden"
|
|
580
|
-
table.show_horizontal_scrollbar = False
|
|
820
|
+
# Always enable horizontal scrollbar
|
|
821
|
+
table.styles.overflow_x = "auto"
|
|
822
|
+
table.show_horizontal_scrollbar = True
|
|
581
823
|
|
|
582
824
|
for event in self._events:
|
|
825
|
+
eid = event["id"]
|
|
826
|
+
share_id = event.get("share_id") or "-"
|
|
583
827
|
title = event["title"]
|
|
584
|
-
if self.wrap_mode:
|
|
585
|
-
title_cell = title
|
|
586
|
-
else:
|
|
587
|
-
title_cell = title[:50] + "..." if len(title) > 50 else title
|
|
588
828
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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")
|
|
595
835
|
|
|
836
|
+
# Column order: ✓, #, Title, Share, Type, Sessions, Event ID, Created
|
|
596
837
|
table.add_row(
|
|
597
|
-
self._checkbox_cell(
|
|
598
|
-
str(event["index"]),
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
event["type"],
|
|
602
|
-
str(event["sessions"]),
|
|
603
|
-
|
|
604
|
-
event["created"],
|
|
605
|
-
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,
|
|
606
847
|
)
|
|
607
848
|
|
|
608
849
|
if table.row_count > 0:
|
|
609
850
|
if selected_event_id:
|
|
610
851
|
try:
|
|
611
|
-
table.cursor_coordinate = (
|
|
852
|
+
table.cursor_coordinate = (
|
|
853
|
+
table.get_row_index(selected_event_id),
|
|
854
|
+
0,
|
|
855
|
+
)
|
|
612
856
|
except Exception:
|
|
613
857
|
table.cursor_coordinate = (0, 0)
|
|
614
858
|
else:
|
|
@@ -618,7 +862,7 @@ class EventsTable(Container, can_focus=True):
|
|
|
618
862
|
total_pages = self._get_total_pages()
|
|
619
863
|
pagination_widget = self.query_one("#pagination-info", Static)
|
|
620
864
|
pagination_widget.update(
|
|
621
|
-
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]"
|
|
622
866
|
)
|
|
623
867
|
|
|
624
868
|
def _shorten_id(self, event_id: str) -> str:
|
|
@@ -23,6 +23,7 @@ class AlineHeader(Static):
|
|
|
23
23
|
"""Get the Aline version."""
|
|
24
24
|
try:
|
|
25
25
|
from importlib.metadata import version
|
|
26
|
+
|
|
26
27
|
return version("aline-ai")
|
|
27
28
|
except Exception:
|
|
28
29
|
return "0.4.1"
|
|
@@ -39,5 +40,5 @@ class AlineHeader(Static):
|
|
|
39
40
|
line3 = " ███████║██║ ██║██╔██╗ ██║█████╗ "
|
|
40
41
|
line4 = " ██╔══██║██║ ██║██║╚██╗██║██╔══╝ "
|
|
41
42
|
line5 = " ██║ ██║███████╗██║██║ ╚████║███████╗"
|
|
42
|
-
info = f"[dim]v{self._version} │ Shared
|
|
43
|
+
info = f"[dim]v{self._version} │ Shared Agent Context[/dim]"
|
|
43
44
|
return f"{line1}\n{line2}\n{line3}\n{line4}\n{line5} {info}"
|
|
@@ -115,7 +115,9 @@ class SearchPanel(Static):
|
|
|
115
115
|
session_id = result.get("session_id") or ""
|
|
116
116
|
turn_id = result.get("turn_id") or ""
|
|
117
117
|
if session_id:
|
|
118
|
-
self.app.push_screen(
|
|
118
|
+
self.app.push_screen(
|
|
119
|
+
SessionDetailScreen(session_id, initial_turn_id=turn_id or None)
|
|
120
|
+
)
|
|
119
121
|
return
|
|
120
122
|
|
|
121
123
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
@@ -156,30 +158,32 @@ class SearchPanel(Static):
|
|
|
156
158
|
|
|
157
159
|
# Search based on type
|
|
158
160
|
if self._search_type in ("all", "event"):
|
|
159
|
-
events = db.search_events(
|
|
160
|
-
self._query, limit=20, use_regex=True, ignore_case=True
|
|
161
|
-
)
|
|
161
|
+
events = db.search_events(self._query, limit=20, use_regex=True, ignore_case=True)
|
|
162
162
|
for event in events:
|
|
163
|
-
self._results.append(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
self._results.append(
|
|
164
|
+
{
|
|
165
|
+
"type": "Event",
|
|
166
|
+
"id": self._shorten_id(event.id),
|
|
167
|
+
"full_id": event.id,
|
|
168
|
+
"title": (event.title or "(no title)")[:60],
|
|
169
|
+
"context": event.event_type or "-",
|
|
170
|
+
}
|
|
171
|
+
)
|
|
170
172
|
|
|
171
173
|
if self._search_type in ("all", "session"):
|
|
172
174
|
sessions = db.search_sessions(
|
|
173
175
|
self._query, limit=20, use_regex=True, ignore_case=True
|
|
174
176
|
)
|
|
175
177
|
for session in sessions:
|
|
176
|
-
self._results.append(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
self._results.append(
|
|
179
|
+
{
|
|
180
|
+
"type": "Session",
|
|
181
|
+
"id": self._shorten_id(session.id),
|
|
182
|
+
"full_id": session.id,
|
|
183
|
+
"title": (session.session_title or "(no title)")[:60],
|
|
184
|
+
"context": session.session_type or "-",
|
|
185
|
+
}
|
|
186
|
+
)
|
|
183
187
|
|
|
184
188
|
if self._search_type in ("all", "turn"):
|
|
185
189
|
turns = db.search_conversations(
|
|
@@ -188,14 +192,18 @@ class SearchPanel(Static):
|
|
|
188
192
|
for turn in turns:
|
|
189
193
|
session_id = turn.get("session_id", "")
|
|
190
194
|
turn_id = turn.get("turn_id", "")
|
|
191
|
-
self._results.append(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
self._results.append(
|
|
196
|
+
{
|
|
197
|
+
"type": "Turn",
|
|
198
|
+
"id": self._shorten_id(turn_id),
|
|
199
|
+
"turn_id": turn_id,
|
|
200
|
+
"session_id": session_id,
|
|
201
|
+
"title": (turn.get("title") or turn.get("summary") or "(no title)")[
|
|
202
|
+
:60
|
|
203
|
+
],
|
|
204
|
+
"context": f"{self._shorten_id(session_id)} • Turn #{turn.get('turn_number', '-')}",
|
|
205
|
+
}
|
|
206
|
+
)
|
|
199
207
|
|
|
200
208
|
self._update_display()
|
|
201
209
|
|
|
@@ -232,7 +240,9 @@ class SearchPanel(Static):
|
|
|
232
240
|
|
|
233
241
|
summary_text = f"Found {len(self._results)} results for '{self._query}'"
|
|
234
242
|
if self._search_type == "all":
|
|
235
|
-
summary_text +=
|
|
243
|
+
summary_text += (
|
|
244
|
+
f" (Events: {event_count}, Sessions: {session_count}, Turns: {turn_count})"
|
|
245
|
+
)
|
|
236
246
|
|
|
237
247
|
summary.update(f"[dim]{summary_text}[/dim]")
|
|
238
248
|
else:
|