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.
Files changed (82) hide show
  1. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/METADATA +1 -1
  2. aline_ai-0.5.6.dist-info/RECORD +95 -0
  3. realign/__init__.py +1 -1
  4. realign/adapters/antigravity.py +28 -20
  5. realign/adapters/base.py +46 -50
  6. realign/adapters/claude.py +14 -14
  7. realign/adapters/codex.py +7 -7
  8. realign/adapters/gemini.py +11 -11
  9. realign/adapters/registry.py +14 -10
  10. realign/claude_detector.py +2 -2
  11. realign/claude_hooks/__init__.py +3 -3
  12. realign/claude_hooks/permission_request_hook_installer.py +31 -32
  13. realign/claude_hooks/stop_hook.py +4 -1
  14. realign/claude_hooks/stop_hook_installer.py +30 -31
  15. realign/cli.py +23 -4
  16. realign/codex_detector.py +11 -11
  17. realign/commands/add.py +88 -65
  18. realign/commands/config.py +3 -12
  19. realign/commands/context.py +3 -1
  20. realign/commands/export_shares.py +86 -127
  21. realign/commands/import_shares.py +145 -155
  22. realign/commands/init.py +166 -30
  23. realign/commands/restore.py +18 -6
  24. realign/commands/search.py +14 -42
  25. realign/commands/upgrade.py +155 -11
  26. realign/commands/watcher.py +98 -219
  27. realign/commands/worker.py +29 -6
  28. realign/config.py +25 -20
  29. realign/context.py +1 -3
  30. realign/dashboard/app.py +34 -24
  31. realign/dashboard/screens/__init__.py +10 -1
  32. realign/dashboard/screens/create_agent.py +244 -0
  33. realign/dashboard/screens/create_event.py +3 -1
  34. realign/dashboard/screens/event_detail.py +14 -6
  35. realign/dashboard/screens/help_screen.py +114 -0
  36. realign/dashboard/screens/session_detail.py +3 -1
  37. realign/dashboard/screens/share_import.py +7 -3
  38. realign/dashboard/tmux_manager.py +54 -9
  39. realign/dashboard/widgets/config_panel.py +85 -1
  40. realign/dashboard/widgets/events_table.py +314 -70
  41. realign/dashboard/widgets/header.py +2 -1
  42. realign/dashboard/widgets/search_panel.py +37 -27
  43. realign/dashboard/widgets/sessions_table.py +404 -85
  44. realign/dashboard/widgets/terminal_panel.py +155 -175
  45. realign/dashboard/widgets/watcher_panel.py +6 -2
  46. realign/dashboard/widgets/worker_panel.py +10 -1
  47. realign/db/__init__.py +1 -1
  48. realign/db/base.py +5 -15
  49. realign/db/locks.py +0 -1
  50. realign/db/migration.py +82 -76
  51. realign/db/schema.py +2 -6
  52. realign/db/sqlite_db.py +23 -41
  53. realign/events/__init__.py +0 -1
  54. realign/events/event_summarizer.py +27 -15
  55. realign/events/session_summarizer.py +29 -15
  56. realign/file_lock.py +1 -0
  57. realign/hooks.py +150 -60
  58. realign/logging_config.py +12 -15
  59. realign/mcp_server.py +30 -51
  60. realign/mcp_watcher.py +0 -1
  61. realign/models/event.py +29 -20
  62. realign/prompts/__init__.py +7 -7
  63. realign/prompts/presets.py +15 -11
  64. realign/redactor.py +99 -59
  65. realign/triggers/__init__.py +9 -9
  66. realign/triggers/antigravity_trigger.py +30 -28
  67. realign/triggers/base.py +4 -3
  68. realign/triggers/claude_trigger.py +104 -85
  69. realign/triggers/codex_trigger.py +15 -5
  70. realign/triggers/gemini_trigger.py +57 -47
  71. realign/triggers/next_turn_trigger.py +3 -1
  72. realign/triggers/registry.py +6 -2
  73. realign/triggers/turn_status.py +3 -1
  74. realign/watcher_core.py +306 -131
  75. realign/watcher_daemon.py +8 -8
  76. realign/worker_core.py +3 -1
  77. realign/worker_daemon.py +3 -1
  78. aline_ai-0.5.4.dist-info/RECORD +0 -93
  79. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/WHEEL +0 -0
  80. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/entry_points.txt +0 -0
  81. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/licenses/LICENSE +0 -0
  82. {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, can_focus=True):
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:focus {
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 .summary-section Button {
75
- width: 12;
76
- margin-left: 1;
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: hidden;
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: hidden;
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 Horizontal(classes="summary-section"):
123
- yield Static(id="events-summary-text")
124
- yield Button("Load ctx (l)", id="load-context-btn", variant="primary")
125
- yield Button("Import (y)", id="share-import-btn", variant="primary")
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(self, event: OpenableDataTable.RowActivated) -> None:
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.add_columns("#", "Event ID", "Title", "Type", "Sessions", "Share", "Created")
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(table.coordinate_to_cell_key(table.cursor_coordinate)[0].value)
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
- elif meta:
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 "[green]☑[/green]" if event_id in self._selected_event_ids else "☐"
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
- summary_widget = self.query_one("#events-summary-text", Static)
294
- summary_text = f"[bold]Total Events:[/bold] {self._total_events}"
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
- summary_text += f" | [bold]Selected:[/bold] {selected_count}"
298
- summary_widget.update(summary_text)
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
- mode = "Wrap" if self.wrap_mode else "Compact"
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(table.coordinate_to_cell_key(table.cursor_coordinate)[0].value)
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
- if self.wrap_mode != self._last_wrap_mode:
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
- if self.wrap_mode:
576
- table.styles.overflow_x = "auto"
577
- table.show_horizontal_scrollbar = True
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
- share_url = event.get("share_url") or ""
590
- share_id = event.get("share_id") or ""
591
- if self.wrap_mode:
592
- share_cell = str(share_url) if share_url else "-"
593
- else:
594
- share_cell = share_id or "-"
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(event["id"]),
598
- str(event["index"]),
599
- event["short_id"],
600
- title_cell,
601
- event["type"],
602
- str(event["sessions"]),
603
- share_cell,
604
- event["created"],
605
- key=event["id"],
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 = (table.get_row_index(selected_event_id), 0)
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 (s) wrap[/dim]"
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 AI Memory[/dim]"
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(SessionDetailScreen(session_id, initial_turn_id=turn_id or None))
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
- "type": "Event",
165
- "id": self._shorten_id(event.id),
166
- "full_id": event.id,
167
- "title": (event.title or "(no title)")[:60],
168
- "context": event.event_type or "-",
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
- "type": "Session",
178
- "id": self._shorten_id(session.id),
179
- "full_id": session.id,
180
- "title": (session.session_title or "(no title)")[:60],
181
- "context": session.session_type or "-",
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
- "type": "Turn",
193
- "id": self._shorten_id(turn_id),
194
- "turn_id": turn_id,
195
- "session_id": session_id,
196
- "title": (turn.get("title") or turn.get("summary") or "(no title)")[:60],
197
- "context": f"{self._shorten_id(session_id)} Turn #{turn.get('turn_number', '-')}",
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 += f" (Events: {event_count}, Sessions: {session_count}, Turns: {turn_count})"
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: