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
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""Sessions 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 pathlib import Path
|
|
5
11
|
from typing import List, Optional, Set
|
|
@@ -7,7 +13,7 @@ from typing import List, Optional, Set
|
|
|
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 SessionsListTable(OpenableDataTable):
|
|
|
44
50
|
self.owner.apply_mouse_selection(row_index, shift=event.shift, meta=event.meta)
|
|
45
51
|
|
|
46
52
|
|
|
47
|
-
class SessionsTable(Container
|
|
53
|
+
class SessionsTable(Container):
|
|
48
54
|
"""Table displaying sessions with keyboard pagination support."""
|
|
49
55
|
|
|
50
56
|
DEFAULT_CSS = """
|
|
@@ -54,26 +60,14 @@ class SessionsTable(Container, can_focus=True):
|
|
|
54
60
|
overflow: hidden;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
SessionsTable
|
|
58
|
-
border: solid $accent;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
SessionsTable .summary-section {
|
|
63
|
+
SessionsTable .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
66
|
}
|
|
68
67
|
|
|
69
|
-
SessionsTable .
|
|
70
|
-
width:
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
SessionsTable .summary-section Button {
|
|
75
|
-
width: 12;
|
|
76
|
-
margin-left: 1;
|
|
68
|
+
SessionsTable .action-section Button {
|
|
69
|
+
width: auto;
|
|
70
|
+
margin-bottom: 1;
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
SessionsTable .section-header {
|
|
@@ -83,13 +77,15 @@ class SessionsTable(Container, can_focus=True):
|
|
|
83
77
|
|
|
84
78
|
SessionsTable .table-container {
|
|
85
79
|
height: auto;
|
|
86
|
-
overflow:
|
|
80
|
+
overflow-x: auto;
|
|
81
|
+
overflow-y: hidden;
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
SessionsTable 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
|
SessionsTable .pagination-info {
|
|
@@ -98,6 +94,12 @@ class SessionsTable(Container, can_focus=True):
|
|
|
98
94
|
color: $text-muted;
|
|
99
95
|
text-align: center;
|
|
100
96
|
}
|
|
97
|
+
|
|
98
|
+
SessionsTable .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,38 @@ class SessionsTable(Container, can_focus=True):
|
|
|
108
110
|
def __init__(self) -> None:
|
|
109
111
|
super().__init__()
|
|
110
112
|
self._sessions: list = []
|
|
113
|
+
self._sessions_by_id: dict = {} # Index sessions by id for quick lookup
|
|
111
114
|
self._total_sessions: int = 0
|
|
112
115
|
self._stats: dict = {}
|
|
113
116
|
self._selected_session_ids: Set[str] = set()
|
|
114
117
|
self._selection_anchor_row: Optional[int] = None
|
|
115
118
|
self._last_wrap_mode: bool = bool(self.wrap_mode)
|
|
116
119
|
self._refresh_worker: Optional[Worker] = None
|
|
120
|
+
self._share_export_worker: Optional[Worker] = None
|
|
117
121
|
self._refresh_timer = None
|
|
118
122
|
self._active_refresh_snapshot: Optional[tuple[int, int]] = None
|
|
119
123
|
self._pending_refresh_snapshot: Optional[tuple[int, int]] = None
|
|
120
124
|
|
|
121
125
|
def compose(self) -> ComposeResult:
|
|
122
126
|
"""Compose the sessions table layout."""
|
|
123
|
-
with
|
|
124
|
-
yield
|
|
125
|
-
|
|
127
|
+
with Vertical(classes="action-section"):
|
|
128
|
+
yield Button(
|
|
129
|
+
"Load selected context to current agent",
|
|
130
|
+
id="load-context-btn",
|
|
131
|
+
variant="primary",
|
|
132
|
+
disabled=True,
|
|
133
|
+
)
|
|
134
|
+
yield Button(
|
|
135
|
+
"Share selected contexts to others",
|
|
136
|
+
id="share-context-btn",
|
|
137
|
+
variant="primary",
|
|
138
|
+
disabled=True,
|
|
139
|
+
)
|
|
126
140
|
yield Static(id="section-header", classes="section-header")
|
|
127
141
|
with Container(classes="table-container"):
|
|
128
142
|
yield SessionsListTable(id="sessions-table")
|
|
129
143
|
yield Static(id="pagination-info", classes="pagination-info")
|
|
144
|
+
yield Static(id="stats-info", classes="stats-info")
|
|
130
145
|
|
|
131
146
|
def on_mount(self) -> None:
|
|
132
147
|
"""Set up the table on mount."""
|
|
@@ -171,7 +186,9 @@ class SessionsTable(Container, can_focus=True):
|
|
|
171
186
|
except Exception:
|
|
172
187
|
pass
|
|
173
188
|
|
|
174
|
-
def on_openable_data_table_row_activated(
|
|
189
|
+
def on_openable_data_table_row_activated(
|
|
190
|
+
self, event: OpenableDataTable.RowActivated
|
|
191
|
+
) -> None:
|
|
175
192
|
if event.data_table.id != "sessions-table":
|
|
176
193
|
return
|
|
177
194
|
|
|
@@ -191,7 +208,13 @@ class SessionsTable(Container, can_focus=True):
|
|
|
191
208
|
def _setup_table_columns(self, table: DataTable) -> None:
|
|
192
209
|
table.clear(columns=True)
|
|
193
210
|
table.add_column("✓", key="sel", width=2)
|
|
194
|
-
table.
|
|
211
|
+
table.add_column("#", key="index", width=3)
|
|
212
|
+
table.add_column("Title", key="title") # Auto width for full title
|
|
213
|
+
table.add_column("Project", key="project", width=15)
|
|
214
|
+
table.add_column("Source", key="source", width=10)
|
|
215
|
+
table.add_column("Turns", key="turns", width=6)
|
|
216
|
+
table.add_column("Session ID", key="session_id", width=20)
|
|
217
|
+
table.add_column("Last Activity", key="last_activity", width=12)
|
|
195
218
|
table.cursor_type = "row"
|
|
196
219
|
|
|
197
220
|
def _sync_to_available_height(self) -> None:
|
|
@@ -220,7 +243,9 @@ class SessionsTable(Container, can_focus=True):
|
|
|
220
243
|
if table.row_count == 0:
|
|
221
244
|
return
|
|
222
245
|
try:
|
|
223
|
-
session_id = str(
|
|
246
|
+
session_id = str(
|
|
247
|
+
table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
|
|
248
|
+
)
|
|
224
249
|
except Exception:
|
|
225
250
|
return
|
|
226
251
|
|
|
@@ -269,19 +294,28 @@ class SessionsTable(Container, can_focus=True):
|
|
|
269
294
|
self._selected_session_ids.update(ids_in_range)
|
|
270
295
|
else:
|
|
271
296
|
self._selected_session_ids = set(ids_in_range)
|
|
272
|
-
|
|
297
|
+
else:
|
|
298
|
+
# Toggle selection on click (no modifier keys needed)
|
|
273
299
|
if clicked_id in self._selected_session_ids:
|
|
274
300
|
self._selected_session_ids.remove(clicked_id)
|
|
275
301
|
else:
|
|
276
302
|
self._selected_session_ids.add(clicked_id)
|
|
277
|
-
else:
|
|
278
|
-
self._selected_session_ids = {clicked_id}
|
|
279
303
|
|
|
280
304
|
self._selection_anchor_row = row_index
|
|
281
305
|
self._refresh_checkboxes_only()
|
|
282
306
|
|
|
283
307
|
def _checkbox_cell(self, session_id: str) -> str:
|
|
284
|
-
return
|
|
308
|
+
return (
|
|
309
|
+
"[bold green]●[/bold green]"
|
|
310
|
+
if session_id in self._selected_session_ids
|
|
311
|
+
else "○"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _format_cell(self, value: str, session_id: str) -> str:
|
|
315
|
+
"""Format cell value with bold if selected."""
|
|
316
|
+
if session_id in self._selected_session_ids:
|
|
317
|
+
return f"[bold]{value}[/bold]"
|
|
318
|
+
return value
|
|
285
319
|
|
|
286
320
|
def _refresh_checkboxes_only(self) -> None:
|
|
287
321
|
table = self.query_one("#sessions-table", DataTable)
|
|
@@ -296,40 +330,80 @@ class SessionsTable(Container, can_focus=True):
|
|
|
296
330
|
continue
|
|
297
331
|
if not sid:
|
|
298
332
|
continue
|
|
333
|
+
session = self._sessions_by_id.get(sid)
|
|
334
|
+
if not session:
|
|
335
|
+
continue
|
|
299
336
|
try:
|
|
337
|
+
# Update all cells in the row with proper formatting
|
|
300
338
|
table.update_cell(sid, "sel", self._checkbox_cell(sid))
|
|
339
|
+
table.update_cell(
|
|
340
|
+
sid, "index", self._format_cell(str(session["index"]), sid)
|
|
341
|
+
)
|
|
342
|
+
table.update_cell(
|
|
343
|
+
sid, "title", self._format_cell(session["title"], sid)
|
|
344
|
+
)
|
|
345
|
+
table.update_cell(
|
|
346
|
+
sid, "project", self._format_cell(session["project"], sid)
|
|
347
|
+
)
|
|
348
|
+
table.update_cell(
|
|
349
|
+
sid, "source", self._format_cell(session["source"], sid)
|
|
350
|
+
)
|
|
351
|
+
table.update_cell(
|
|
352
|
+
sid, "turns", self._format_cell(str(session["turns"]), sid)
|
|
353
|
+
)
|
|
354
|
+
table.update_cell(
|
|
355
|
+
sid, "session_id", self._format_cell(session["short_id"], sid)
|
|
356
|
+
)
|
|
357
|
+
table.update_cell(
|
|
358
|
+
sid,
|
|
359
|
+
"last_activity",
|
|
360
|
+
self._format_cell(session["last_activity"], sid),
|
|
361
|
+
)
|
|
301
362
|
except Exception:
|
|
302
363
|
continue
|
|
303
364
|
|
|
304
365
|
self._update_summary_widget()
|
|
305
366
|
|
|
306
367
|
def _update_summary_widget(self) -> None:
|
|
307
|
-
|
|
368
|
+
# Update stats at the bottom
|
|
369
|
+
stats_widget = self.query_one("#stats-info", Static)
|
|
308
370
|
total = self._stats.get("total", 0)
|
|
309
371
|
claude = self._stats.get("claude", 0)
|
|
310
372
|
codex = self._stats.get("codex", 0)
|
|
311
373
|
gemini = self._stats.get("gemini", 0)
|
|
312
374
|
|
|
313
|
-
|
|
375
|
+
stats_parts = [f"Total: {total}"]
|
|
314
376
|
if claude > 0:
|
|
315
|
-
|
|
377
|
+
stats_parts.append(f"Claude: {claude}")
|
|
316
378
|
if codex > 0:
|
|
317
|
-
|
|
379
|
+
stats_parts.append(f"Codex: {codex}")
|
|
318
380
|
if gemini > 0:
|
|
319
|
-
|
|
381
|
+
stats_parts.append(f"Gemini: {gemini}")
|
|
320
382
|
|
|
321
383
|
selected_count = len(self._selected_session_ids)
|
|
322
384
|
if selected_count:
|
|
323
|
-
|
|
385
|
+
stats_parts.append(f"Selected: {selected_count}")
|
|
324
386
|
|
|
325
|
-
|
|
387
|
+
stats_widget.update(f"[dim]{' | '.join(stats_parts)}[/dim]")
|
|
388
|
+
|
|
389
|
+
# Enable/disable buttons based on selection
|
|
390
|
+
load_btn = self.query_one("#load-context-btn", Button)
|
|
391
|
+
load_btn.disabled = selected_count == 0
|
|
392
|
+
share_btn = self.query_one("#share-context-btn", Button)
|
|
393
|
+
share_btn.disabled = selected_count == 0
|
|
326
394
|
|
|
327
395
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
328
|
-
|
|
396
|
+
button_id = event.button.id or ""
|
|
397
|
+
|
|
398
|
+
if button_id == "load-context-btn":
|
|
399
|
+
action = getattr(self.app, "action_load_context", None)
|
|
400
|
+
if callable(action):
|
|
401
|
+
await action()
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
if button_id == "share-context-btn":
|
|
405
|
+
self._start_share_export()
|
|
329
406
|
return
|
|
330
|
-
action = getattr(self.app, "action_load_context", None)
|
|
331
|
-
if callable(action):
|
|
332
|
-
await action()
|
|
333
407
|
|
|
334
408
|
def _calculate_rows_per_page(self) -> None:
|
|
335
409
|
"""Calculate rows per page based on available height."""
|
|
@@ -407,6 +481,15 @@ class SessionsTable(Container, can_focus=True):
|
|
|
407
481
|
self.refresh_data()
|
|
408
482
|
|
|
409
483
|
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
484
|
+
# Handle share export worker
|
|
485
|
+
if (
|
|
486
|
+
self._share_export_worker is not None
|
|
487
|
+
and event.worker is self._share_export_worker
|
|
488
|
+
):
|
|
489
|
+
self._on_share_export_worker_changed(event)
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
# Handle refresh worker
|
|
410
493
|
if self._refresh_worker is None or event.worker is not self._refresh_worker:
|
|
411
494
|
return
|
|
412
495
|
|
|
@@ -434,13 +517,243 @@ class SessionsTable(Container, can_focus=True):
|
|
|
434
517
|
self._stats = {}
|
|
435
518
|
try:
|
|
436
519
|
self._sessions = list(result.get("sessions") or [])
|
|
520
|
+
self._sessions_by_id = {s["id"]: s for s in self._sessions}
|
|
437
521
|
except Exception:
|
|
438
522
|
self._sessions = []
|
|
523
|
+
self._sessions_by_id = {}
|
|
439
524
|
self._update_display()
|
|
440
525
|
|
|
441
526
|
if self._pending_refresh_snapshot is not None:
|
|
442
527
|
self.refresh_data()
|
|
443
528
|
|
|
529
|
+
def _start_share_export(self) -> None:
|
|
530
|
+
"""Generate an event from selected sessions and export as share link."""
|
|
531
|
+
selected_ids = list(self._selected_session_ids)
|
|
532
|
+
if not selected_ids:
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
if (
|
|
536
|
+
self._share_export_worker is not None
|
|
537
|
+
and self._share_export_worker.state
|
|
538
|
+
in (
|
|
539
|
+
WorkerState.PENDING,
|
|
540
|
+
WorkerState.RUNNING,
|
|
541
|
+
)
|
|
542
|
+
):
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
def work() -> dict:
|
|
546
|
+
# Step 1: Generate event from selected sessions
|
|
547
|
+
session_selector = ",".join(selected_ids)
|
|
548
|
+
stdout_gen = io.StringIO()
|
|
549
|
+
stderr_gen = io.StringIO()
|
|
550
|
+
|
|
551
|
+
with (
|
|
552
|
+
contextlib.redirect_stdout(stdout_gen),
|
|
553
|
+
contextlib.redirect_stderr(stderr_gen),
|
|
554
|
+
):
|
|
555
|
+
from ...commands.watcher import watcher_event_generate_command
|
|
556
|
+
|
|
557
|
+
gen_exit_code = watcher_event_generate_command(
|
|
558
|
+
session_selector=session_selector,
|
|
559
|
+
show_sessions=False,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if gen_exit_code != 0:
|
|
563
|
+
return {
|
|
564
|
+
"step": "generate",
|
|
565
|
+
"exit_code": gen_exit_code,
|
|
566
|
+
"stderr": stderr_gen.getvalue().strip(),
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
# Extract event_id from output
|
|
570
|
+
gen_output = stdout_gen.getvalue().strip()
|
|
571
|
+
event_id = None
|
|
572
|
+
for line in gen_output.split("\n"):
|
|
573
|
+
if line.startswith("Event ID:"):
|
|
574
|
+
event_id = line.split(":", 1)[1].strip()
|
|
575
|
+
break
|
|
576
|
+
|
|
577
|
+
if not event_id:
|
|
578
|
+
# Try to find the most recent event
|
|
579
|
+
try:
|
|
580
|
+
from ...db import get_database
|
|
581
|
+
|
|
582
|
+
db = get_database()
|
|
583
|
+
conn = db._get_connection()
|
|
584
|
+
row = conn.execute(
|
|
585
|
+
"SELECT id FROM events ORDER BY created_at DESC LIMIT 1"
|
|
586
|
+
).fetchone()
|
|
587
|
+
if row:
|
|
588
|
+
event_id = row[0]
|
|
589
|
+
except Exception:
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
if not event_id:
|
|
593
|
+
return {
|
|
594
|
+
"step": "generate",
|
|
595
|
+
"exit_code": 1,
|
|
596
|
+
"stderr": "Could not determine event ID",
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
# Step 2: Export the event as a share link
|
|
600
|
+
stdout_exp = io.StringIO()
|
|
601
|
+
stderr_exp = io.StringIO()
|
|
602
|
+
|
|
603
|
+
with (
|
|
604
|
+
contextlib.redirect_stdout(stdout_exp),
|
|
605
|
+
contextlib.redirect_stderr(stderr_exp),
|
|
606
|
+
):
|
|
607
|
+
from ...commands import export_shares
|
|
608
|
+
|
|
609
|
+
exp_exit_code = export_shares.export_shares_interactive_command(
|
|
610
|
+
indices=event_id,
|
|
611
|
+
password=None,
|
|
612
|
+
enable_preview=False,
|
|
613
|
+
json_output=True,
|
|
614
|
+
compact=True,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
exp_output = stdout_exp.getvalue().strip()
|
|
618
|
+
result: dict = {
|
|
619
|
+
"step": "export",
|
|
620
|
+
"exit_code": exp_exit_code,
|
|
621
|
+
"output": exp_output,
|
|
622
|
+
"stderr": stderr_exp.getvalue().strip(),
|
|
623
|
+
"event_id": event_id,
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if exp_output:
|
|
627
|
+
# Try to extract JSON from output (may contain other text)
|
|
628
|
+
try:
|
|
629
|
+
result["json"] = json.loads(exp_output)
|
|
630
|
+
except Exception:
|
|
631
|
+
# Try to find JSON object in output
|
|
632
|
+
json_start = exp_output.find("{")
|
|
633
|
+
json_end = exp_output.rfind("}") + 1
|
|
634
|
+
if json_start >= 0 and json_end > json_start:
|
|
635
|
+
try:
|
|
636
|
+
result["json"] = json.loads(exp_output[json_start:json_end])
|
|
637
|
+
except Exception:
|
|
638
|
+
result["json"] = None
|
|
639
|
+
else:
|
|
640
|
+
result["json"] = None
|
|
641
|
+
else:
|
|
642
|
+
result["json"] = None
|
|
643
|
+
|
|
644
|
+
return result
|
|
645
|
+
|
|
646
|
+
self.app.notify("Creating share link...", title="Share", timeout=2)
|
|
647
|
+
self._share_export_worker = self.run_worker(
|
|
648
|
+
work, thread=True, exit_on_error=False
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def _on_share_export_worker_changed(self, event: Worker.StateChanged) -> None:
|
|
652
|
+
"""Handle share export worker state changes."""
|
|
653
|
+
if event.state == WorkerState.ERROR:
|
|
654
|
+
err = (
|
|
655
|
+
self._share_export_worker.error
|
|
656
|
+
if self._share_export_worker
|
|
657
|
+
else "Unknown error"
|
|
658
|
+
)
|
|
659
|
+
self.app.notify(
|
|
660
|
+
f"Share export failed: {err}", title="Share", severity="error"
|
|
661
|
+
)
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
if event.state != WorkerState.SUCCESS:
|
|
665
|
+
return
|
|
666
|
+
|
|
667
|
+
result = self._share_export_worker.result if self._share_export_worker else {}
|
|
668
|
+
if not result:
|
|
669
|
+
result = {}
|
|
670
|
+
|
|
671
|
+
step = result.get("step", "")
|
|
672
|
+
exit_code = int(result.get("exit_code", 1))
|
|
673
|
+
|
|
674
|
+
if step == "generate" and exit_code != 0:
|
|
675
|
+
stderr = result.get("stderr", "")
|
|
676
|
+
self.app.notify(
|
|
677
|
+
f"Failed to generate event: {stderr}"
|
|
678
|
+
if stderr
|
|
679
|
+
else "Failed to generate event",
|
|
680
|
+
title="Share",
|
|
681
|
+
severity="error",
|
|
682
|
+
)
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
if exit_code != 0:
|
|
686
|
+
stderr = result.get("stderr", "")
|
|
687
|
+
self.app.notify(
|
|
688
|
+
f"Share export failed: {stderr}" if stderr else "Share export failed",
|
|
689
|
+
title="Share",
|
|
690
|
+
severity="error",
|
|
691
|
+
)
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
payload = result.get("json") or {}
|
|
695
|
+
share_link = payload.get("share_link") or payload.get("share_url")
|
|
696
|
+
slack_message = (
|
|
697
|
+
payload.get("slack_message") if isinstance(payload, dict) else None
|
|
698
|
+
)
|
|
699
|
+
event_id = result.get("event_id")
|
|
700
|
+
|
|
701
|
+
# Try to fetch share_link and slack_message from database
|
|
702
|
+
if event_id:
|
|
703
|
+
try:
|
|
704
|
+
from ...db import get_database
|
|
705
|
+
|
|
706
|
+
db = get_database()
|
|
707
|
+
event = db.get_event_by_id(event_id)
|
|
708
|
+
if event:
|
|
709
|
+
if not share_link:
|
|
710
|
+
share_link = getattr(event, "share_url", None)
|
|
711
|
+
if not slack_message:
|
|
712
|
+
slack_message = getattr(event, "slack_message", None)
|
|
713
|
+
except Exception:
|
|
714
|
+
pass
|
|
715
|
+
|
|
716
|
+
if not share_link:
|
|
717
|
+
self.app.notify(
|
|
718
|
+
"Share export completed but no link generated",
|
|
719
|
+
title="Share",
|
|
720
|
+
severity="warning",
|
|
721
|
+
)
|
|
722
|
+
return
|
|
723
|
+
|
|
724
|
+
# Build copy text
|
|
725
|
+
if slack_message:
|
|
726
|
+
copy_text = str(slack_message) + "\n\n" + str(share_link)
|
|
727
|
+
else:
|
|
728
|
+
copy_text = str(share_link)
|
|
729
|
+
|
|
730
|
+
# Copy to clipboard
|
|
731
|
+
copied = False
|
|
732
|
+
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
733
|
+
try:
|
|
734
|
+
copied = (
|
|
735
|
+
subprocess.run(
|
|
736
|
+
["pbcopy"],
|
|
737
|
+
input=copy_text,
|
|
738
|
+
text=True,
|
|
739
|
+
capture_output=False,
|
|
740
|
+
check=False,
|
|
741
|
+
).returncode
|
|
742
|
+
== 0
|
|
743
|
+
)
|
|
744
|
+
except Exception:
|
|
745
|
+
copied = False
|
|
746
|
+
|
|
747
|
+
if not copied:
|
|
748
|
+
try:
|
|
749
|
+
self.app.copy_to_clipboard(copy_text)
|
|
750
|
+
copied = True
|
|
751
|
+
except Exception:
|
|
752
|
+
copied = False
|
|
753
|
+
|
|
754
|
+
suffix = " (copied to clipboard)" if copied else ""
|
|
755
|
+
self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
|
|
756
|
+
|
|
444
757
|
def _collect_snapshot(self, page: int, rows_per_page: int) -> dict:
|
|
445
758
|
"""Collect sessions + stats for a single page (background thread)."""
|
|
446
759
|
total_sessions: int = 0
|
|
@@ -552,8 +865,7 @@ class SessionsTable(Container, can_focus=True):
|
|
|
552
865
|
|
|
553
866
|
# Update section header
|
|
554
867
|
header_widget = self.query_one("#section-header", Static)
|
|
555
|
-
|
|
556
|
-
header_widget.update(f"[bold]Sessions[/bold] [dim]({mode})[/dim]")
|
|
868
|
+
header_widget.update("[bold]Sessions[/bold]")
|
|
557
869
|
|
|
558
870
|
# Update table
|
|
559
871
|
table = self.query_one("#sessions-table", DataTable)
|
|
@@ -565,44 +877,42 @@ class SessionsTable(Container, can_focus=True):
|
|
|
565
877
|
)
|
|
566
878
|
except Exception:
|
|
567
879
|
selected_session_id = None
|
|
568
|
-
|
|
569
|
-
self._setup_table_columns(table)
|
|
570
|
-
self._last_wrap_mode = bool(self.wrap_mode)
|
|
571
|
-
else:
|
|
572
|
-
table.clear()
|
|
880
|
+
table.clear()
|
|
573
881
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
else:
|
|
578
|
-
table.styles.overflow_x = "hidden"
|
|
579
|
-
table.show_horizontal_scrollbar = False
|
|
882
|
+
# Always enable horizontal scrollbar
|
|
883
|
+
table.styles.overflow_x = "auto"
|
|
884
|
+
table.show_horizontal_scrollbar = True
|
|
580
885
|
|
|
581
886
|
for session in self._sessions:
|
|
887
|
+
sid = session["id"]
|
|
582
888
|
title = session["title"]
|
|
889
|
+
if not self.wrap_mode and len(title) > 60:
|
|
890
|
+
title = title[:57] + "..."
|
|
891
|
+
|
|
892
|
+
display_id = session["short_id"]
|
|
583
893
|
if self.wrap_mode:
|
|
584
|
-
|
|
585
|
-
session_id_cell = session["id"]
|
|
586
|
-
else:
|
|
587
|
-
title_cell = title[:40] + "..." if len(title) > 40 else title
|
|
588
|
-
session_id_cell = session["short_id"]
|
|
894
|
+
display_id = sid
|
|
589
895
|
|
|
896
|
+
# Column order: ✓, #, Title, Project, Source, Turns, Session ID, Last Activity
|
|
590
897
|
table.add_row(
|
|
591
|
-
self._checkbox_cell(
|
|
592
|
-
str(session["index"]),
|
|
593
|
-
|
|
594
|
-
session["
|
|
595
|
-
session["
|
|
596
|
-
str(session["turns"]),
|
|
597
|
-
|
|
598
|
-
session["last_activity"],
|
|
599
|
-
key=
|
|
898
|
+
self._checkbox_cell(sid),
|
|
899
|
+
self._format_cell(str(session["index"]), sid),
|
|
900
|
+
self._format_cell(title, sid),
|
|
901
|
+
self._format_cell(session["project"], sid),
|
|
902
|
+
self._format_cell(session["source"], sid),
|
|
903
|
+
self._format_cell(str(session["turns"]), sid),
|
|
904
|
+
self._format_cell(display_id, sid),
|
|
905
|
+
self._format_cell(session["last_activity"], sid),
|
|
906
|
+
key=sid,
|
|
600
907
|
)
|
|
601
908
|
|
|
602
909
|
if table.row_count > 0:
|
|
603
910
|
if selected_session_id:
|
|
604
911
|
try:
|
|
605
|
-
table.cursor_coordinate = (
|
|
912
|
+
table.cursor_coordinate = (
|
|
913
|
+
table.get_row_index(selected_session_id),
|
|
914
|
+
0,
|
|
915
|
+
)
|
|
606
916
|
except Exception:
|
|
607
917
|
table.cursor_coordinate = (0, 0)
|
|
608
918
|
else:
|
|
@@ -612,7 +922,7 @@ class SessionsTable(Container, can_focus=True):
|
|
|
612
922
|
total_pages = self._get_total_pages()
|
|
613
923
|
pagination_widget = self.query_one("#pagination-info", Static)
|
|
614
924
|
pagination_widget.update(
|
|
615
|
-
f"[dim]Page {self.current_page}/{total_pages} ({self._total_sessions} total) │ (p) prev (n) next
|
|
925
|
+
f"[dim]Page {self.current_page}/{total_pages} ({self._total_sessions} total) │ (p) prev (n) next[/dim]"
|
|
616
926
|
)
|
|
617
927
|
|
|
618
928
|
def _shorten_session_id(self, session_id: str) -> str:
|