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
@@ -11,7 +11,6 @@ import asyncio
11
11
  import os
12
12
  import re
13
13
  import shlex
14
- import subprocess
15
14
  from pathlib import Path
16
15
  from typing import Callable
17
16
 
@@ -61,8 +60,7 @@ class _SignalFileWatcher:
61
60
  try:
62
61
  if PERMISSION_SIGNAL_DIR.exists():
63
62
  self._seen_files = {
64
- f.name for f in PERMISSION_SIGNAL_DIR.iterdir()
65
- if f.suffix == ".signal"
63
+ f.name for f in PERMISSION_SIGNAL_DIR.iterdir() if f.suffix == ".signal"
66
64
  }
67
65
  except Exception:
68
66
  self._seen_files = set()
@@ -89,8 +87,7 @@ class _SignalFileWatcher:
89
87
 
90
88
  # Check for new signal files
91
89
  current_files = {
92
- f.name for f in PERMISSION_SIGNAL_DIR.iterdir()
93
- if f.suffix == ".signal"
90
+ f.name for f in PERMISSION_SIGNAL_DIR.iterdir() if f.suffix == ".signal"
94
91
  }
95
92
  new_files = current_files - self._seen_files
96
93
 
@@ -115,7 +112,7 @@ class _SignalFileWatcher:
115
112
  files = sorted(
116
113
  PERMISSION_SIGNAL_DIR.glob("*.signal"),
117
114
  key=lambda f: f.stat().st_mtime,
118
- reverse=True
115
+ reverse=True,
119
116
  )
120
117
  # Keep only the 10 most recent
121
118
  for f in files[10:]:
@@ -132,6 +129,7 @@ class TerminalPanel(Container, can_focus=True):
132
129
 
133
130
  class PermissionRequestDetected(Message):
134
131
  """Posted when a new permission request signal file is detected."""
132
+
135
133
  pass
136
134
 
137
135
  DEFAULT_CSS = """
@@ -295,14 +293,17 @@ class TerminalPanel(Container, can_focus=True):
295
293
  def compose(self) -> ComposeResult:
296
294
  controls_enabled = self.supported()
297
295
  with Horizontal(classes="summary"):
298
- yield Button("+ Claude", id="new-cc", variant="primary", disabled=not controls_enabled)
299
- yield Button("+ Codex", id="new-codex", variant="primary", disabled=not controls_enabled)
300
- yield Button("+ zsh", id="new-zsh", variant="primary", disabled=not controls_enabled)
301
- yield Button("", id="refresh")
302
- yield Static("", id="status", classes="status")
296
+ yield Button(
297
+ "+ Create",
298
+ id="new-agent",
299
+ variant="primary",
300
+ disabled=not controls_enabled,
301
+ )
303
302
  with Vertical(id="terminals", classes="list"):
304
303
  if controls_enabled:
305
- yield Static("No terminals yet. Click 'New cc' / 'New codex' to open the right pane.")
304
+ yield Static(
305
+ "No terminals yet. Click 'Create' to open a new agent terminal."
306
+ )
306
307
  else:
307
308
  yield Static(self._support_message())
308
309
 
@@ -356,21 +357,15 @@ class TerminalPanel(Container, can_focus=True):
356
357
  async with self._refresh_lock:
357
358
  try:
358
359
  supported = self.supported()
359
- except Exception as e:
360
- self._set_status(f"Terminal support check failed: {e}")
360
+ except Exception:
361
361
  return
362
362
 
363
363
  if not supported:
364
- try:
365
- self._set_status(self._support_message())
366
- except Exception:
367
- self._set_status("Terminal not supported")
368
364
  return
369
365
 
370
366
  try:
371
367
  windows = tmux_manager.list_inner_windows()
372
- except Exception as e:
373
- self._set_status(f"Failed to query tmux windows: {e}")
368
+ except Exception:
374
369
  return
375
370
  active_window_id = next((w.window_id for w in windows if w.active), None)
376
371
  if self._expanded_window_id and self._expanded_window_id != active_window_id:
@@ -385,10 +380,16 @@ class TerminalPanel(Container, can_focus=True):
385
380
  for w in windows:
386
381
  if not self._is_claude_window(w) or not w.context_id:
387
382
  continue
388
- session_ids, session_count, event_count = self._get_loaded_context_info(w.context_id)
383
+ session_ids, session_count, event_count = self._get_loaded_context_info(
384
+ w.context_id
385
+ )
389
386
  if not session_ids and session_count == 0 and event_count == 0:
390
387
  continue
391
- context_info_by_context_id[w.context_id] = (session_ids, session_count, event_count)
388
+ context_info_by_context_id[w.context_id] = (
389
+ session_ids,
390
+ session_count,
391
+ event_count,
392
+ )
392
393
  all_context_session_ids.update(session_ids)
393
394
 
394
395
  if all_context_session_ids:
@@ -396,10 +397,8 @@ class TerminalPanel(Container, can_focus=True):
396
397
 
397
398
  try:
398
399
  await self._render_terminals(windows, titles, context_info_by_context_id)
399
- except Exception as e:
400
- self._set_status(f"Failed to render terminal list: {e}")
400
+ except Exception:
401
401
  return
402
- self._set_status(f"{len(windows)} terminals")
403
402
 
404
403
  def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
405
404
  if not session_ids:
@@ -464,12 +463,6 @@ class TerminalPanel(Container, can_focus=True):
464
463
  except Exception:
465
464
  return ([], 0, 0)
466
465
 
467
- def _set_status(self, text: str) -> None:
468
- try:
469
- self.query_one("#status", Static).update(text)
470
- except Exception:
471
- pass
472
-
473
466
  async def _render_terminals(
474
467
  self,
475
468
  windows: list[tmux_manager.InnerWindow],
@@ -481,7 +474,7 @@ class TerminalPanel(Container, can_focus=True):
481
474
 
482
475
  if not windows:
483
476
  await container.mount(
484
- Static("No terminals yet. Click 'New cc' / 'New codex' to open the right pane.")
477
+ Static("No terminals yet. Click 'Create' to open a new agent terminal.")
485
478
  )
486
479
  return
487
480
 
@@ -516,9 +509,7 @@ class TerminalPanel(Container, can_focus=True):
516
509
  if w.active and can_toggle_ctx:
517
510
  await row.mount(
518
511
  Button(
519
- # Avoid Rich crash when Textual measures with width=0 (can happen during
520
- # initial layout / tab switching).
521
- "⮟" if expanded else "⮞",
512
+ "▼" if expanded else "▶",
522
513
  id=f"toggle-{safe}",
523
514
  name=w.window_id,
524
515
  variant="default",
@@ -536,12 +527,7 @@ class TerminalPanel(Container, can_focus=True):
536
527
  )
537
528
  )
538
529
 
539
- if (
540
- w.active
541
- and self._is_claude_window(w)
542
- and w.context_id
543
- and expanded
544
- ):
530
+ if w.active and self._is_claude_window(w) and w.context_id and expanded:
545
531
  ctx = VerticalScroll(id=f"ctx-{safe}", classes="context-sessions")
546
532
  await container.mount(ctx)
547
533
  if loaded_ids:
@@ -591,16 +577,17 @@ class TerminalPanel(Container, can_focus=True):
591
577
  details.append(header)
592
578
  details.append("\n")
593
579
 
594
- # Include window_name to distinguish terminals with same session_id
595
- window_name = w.window_name or ""
596
- detail_line = f"[{window_name}]" if window_name else "claude"
580
+ # Build detail line with Claude label
581
+ detail_line = "[Claude]"
597
582
  if w.session_id:
598
583
  detail_line = f"{detail_line} #{self._short_id(w.session_id)}"
599
584
  if w.active:
600
585
  loaded_count = raw_sessions + raw_events
601
586
  detail_line = f"{detail_line} | loaded context: {loaded_count}"
602
587
  else:
603
- detail_line = f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
588
+ detail_line = (
589
+ f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
590
+ )
604
591
  details.append(detail_line, style="dim not bold")
605
592
  return details
606
593
 
@@ -621,156 +608,149 @@ class TerminalPanel(Container, can_focus=True):
621
608
  return f"w-{safe}"
622
609
  return safe
623
610
 
624
- async def _select_workspace(self, prompt: str = "Select workspace") -> str | None:
625
- """Open macOS folder picker and return selected path, or None if cancelled."""
626
- try:
627
- default_path = os.getcwd()
628
- except Exception:
629
- # Can happen if the original working directory was deleted/moved.
630
- default_path = str(Path.home())
631
- # Use osascript to invoke macOS native folder picker
632
- default_path_escaped = default_path.replace('"', '\\"')
633
- prompt_escaped = prompt.replace('"', '\\"')
634
- script = f'''
635
- set defaultFolder to POSIX file "{default_path_escaped}" as alias
636
- try
637
- set selectedFolder to choose folder with prompt "{prompt_escaped}" default location defaultFolder
638
- return POSIX path of selectedFolder
639
- on error
640
- return ""
641
- end try
642
- '''
643
- try:
644
- proc = await asyncio.get_event_loop().run_in_executor(
645
- None,
646
- lambda: subprocess.run(
647
- ["osascript", "-e", script],
648
- capture_output=True,
649
- text=True,
650
- timeout=120,
651
- ),
652
- )
653
- result = (proc.stdout or "").strip()
654
- if result and os.path.isdir(result):
655
- return result
656
- return None
657
- except Exception:
658
- return None
659
-
660
611
  @staticmethod
661
612
  def _command_in_directory(command: str, directory: str) -> str:
662
613
  """Wrap a command to run in a specific directory."""
663
614
  return f"cd {shlex.quote(directory)} && {command}"
664
615
 
665
- async def on_button_pressed(self, event: Button.Pressed) -> None:
666
- button_id = event.button.id or ""
667
-
668
- if button_id == "refresh":
669
- await self.refresh_data()
616
+ def _on_create_agent_result(self, result: tuple[str, str] | None) -> None:
617
+ """Handle the result from CreateAgentScreen modal."""
618
+ if result is None:
670
619
  return
671
620
 
672
- if not self.supported():
673
- self.app.notify(
674
- self._support_message(),
675
- title="Terminal",
676
- severity="warning",
677
- )
678
- return
679
-
680
- if button_id == "new-cc":
681
- workspace = await self._select_workspace("Select workspace for Claude")
682
- if not workspace:
683
- return
621
+ agent_type, workspace = result
622
+ self.run_worker(
623
+ self._create_agent(agent_type, workspace),
624
+ group="terminal-panel-create",
625
+ exclusive=True,
626
+ )
684
627
 
685
- terminal_id = tmux_manager.new_terminal_id()
686
- context_id = tmux_manager.new_context_id("cc")
687
- env = {
688
- tmux_manager.ENV_TERMINAL_ID: terminal_id,
689
- tmux_manager.ENV_TERMINAL_PROVIDER: "claude",
690
- tmux_manager.ENV_INNER_SOCKET: tmux_manager.INNER_SOCKET,
691
- tmux_manager.ENV_INNER_SESSION: tmux_manager.INNER_SESSION,
692
- tmux_manager.ENV_CONTEXT_ID: context_id,
693
- }
628
+ async def _create_agent(self, agent_type: str, workspace: str) -> None:
629
+ """Create a new agent terminal based on the selected type and workspace."""
630
+ if agent_type == "claude":
631
+ await self._create_claude_terminal(workspace)
632
+ elif agent_type == "codex":
633
+ await self._create_codex_terminal(workspace)
634
+ elif agent_type == "opencode":
635
+ await self._create_opencode_terminal(workspace)
636
+ elif agent_type == "zsh":
637
+ await self._create_zsh_terminal(workspace)
638
+ await self.refresh_data()
639
+
640
+ async def _create_claude_terminal(self, workspace: str) -> None:
641
+ """Create a new Claude terminal."""
642
+ terminal_id = tmux_manager.new_terminal_id()
643
+ context_id = tmux_manager.new_context_id("cc")
644
+ env = {
645
+ tmux_manager.ENV_TERMINAL_ID: terminal_id,
646
+ tmux_manager.ENV_TERMINAL_PROVIDER: "claude",
647
+ tmux_manager.ENV_INNER_SOCKET: tmux_manager.INNER_SOCKET,
648
+ tmux_manager.ENV_INNER_SESSION: tmux_manager.INNER_SESSION,
649
+ tmux_manager.ENV_CONTEXT_ID: context_id,
650
+ }
694
651
 
695
- try:
696
- from pathlib import Path
652
+ try:
653
+ from ...claude_hooks.stop_hook_installer import (
654
+ ensure_stop_hook_installed,
655
+ get_settings_path as get_stop_settings_path,
656
+ install_stop_hook,
657
+ )
658
+ from ...claude_hooks.user_prompt_submit_hook_installer import (
659
+ ensure_user_prompt_submit_hook_installed,
660
+ get_settings_path as get_submit_settings_path,
661
+ install_user_prompt_submit_hook,
662
+ )
663
+ from ...claude_hooks.permission_request_hook_installer import (
664
+ ensure_permission_request_hook_installed,
665
+ get_settings_path as get_permission_settings_path,
666
+ install_permission_request_hook,
667
+ )
697
668
 
698
- from ...claude_hooks.stop_hook_installer import (
699
- ensure_stop_hook_installed,
700
- get_settings_path as get_stop_settings_path,
701
- install_stop_hook,
702
- )
703
- from ...claude_hooks.user_prompt_submit_hook_installer import (
704
- ensure_user_prompt_submit_hook_installed,
705
- get_settings_path as get_submit_settings_path,
706
- install_user_prompt_submit_hook,
707
- )
708
- from ...claude_hooks.permission_request_hook_installer import (
709
- ensure_permission_request_hook_installed,
710
- get_settings_path as get_permission_settings_path,
711
- install_permission_request_hook,
712
- )
669
+ ok_global_stop = ensure_stop_hook_installed(quiet=True)
670
+ ok_global_submit = ensure_user_prompt_submit_hook_installed(quiet=True)
671
+ ok_global_permission = ensure_permission_request_hook_installed(quiet=True)
713
672
 
714
- ok_global_stop = ensure_stop_hook_installed(quiet=True)
715
- ok_global_submit = ensure_user_prompt_submit_hook_installed(quiet=True)
716
- ok_global_permission = ensure_permission_request_hook_installed(quiet=True)
673
+ project_root = Path(workspace)
674
+ ok_project_stop = install_stop_hook(
675
+ get_stop_settings_path(project_root), quiet=True
676
+ )
677
+ ok_project_submit = install_user_prompt_submit_hook(
678
+ get_submit_settings_path(project_root), quiet=True
679
+ )
680
+ ok_project_permission = install_permission_request_hook(
681
+ get_permission_settings_path(project_root), quiet=True
682
+ )
717
683
 
718
- project_root = Path(workspace)
719
- ok_project_stop = install_stop_hook(get_stop_settings_path(project_root), quiet=True)
720
- ok_project_submit = install_user_prompt_submit_hook(
721
- get_submit_settings_path(project_root), quiet=True
722
- )
723
- ok_project_permission = install_permission_request_hook(
724
- get_permission_settings_path(project_root), quiet=True
684
+ all_hooks_ok = (
685
+ ok_global_stop
686
+ and ok_global_submit
687
+ and ok_global_permission
688
+ and ok_project_stop
689
+ and ok_project_submit
690
+ and ok_project_permission
691
+ )
692
+ if not all_hooks_ok:
693
+ self.app.notify(
694
+ "Claude hooks not fully installed; session id/title may not update",
695
+ title="Terminal",
696
+ severity="warning",
725
697
  )
698
+ except Exception:
699
+ pass
726
700
 
727
- all_hooks_ok = (
728
- ok_global_stop and ok_global_submit and ok_global_permission
729
- and ok_project_stop and ok_project_submit and ok_project_permission
730
- )
731
- if not all_hooks_ok:
732
- self.app.notify(
733
- "Claude hooks not fully installed; session id/title may not update",
734
- title="Terminal",
735
- severity="warning",
736
- )
737
- except Exception:
738
- pass
701
+ command = self._command_in_directory(
702
+ tmux_manager.zsh_run_and_keep_open("claude"), workspace
703
+ )
704
+ created = tmux_manager.create_inner_window(
705
+ "cc",
706
+ tmux_manager.shell_command_with_env(command, env),
707
+ terminal_id=terminal_id,
708
+ provider="claude",
709
+ context_id=context_id,
710
+ )
711
+ if not created:
712
+ self.app.notify("Failed to open Claude terminal", title="Terminal", severity="error")
739
713
 
740
- command = self._command_in_directory(
741
- tmux_manager.zsh_run_and_keep_open("claude"), workspace
742
- )
743
- created = tmux_manager.create_inner_window(
744
- "cc",
745
- tmux_manager.shell_command_with_env(command, env),
746
- terminal_id=terminal_id,
747
- provider="claude",
748
- context_id=context_id,
749
- )
750
- if not created:
751
- self.app.notify("Failed to open cc terminal", title="Terminal", severity="error")
752
- await self.refresh_data()
753
- return
714
+ async def _create_codex_terminal(self, workspace: str) -> None:
715
+ """Create a new Codex terminal."""
716
+ command = self._command_in_directory(
717
+ tmux_manager.zsh_run_and_keep_open("codex"), workspace
718
+ )
719
+ created = tmux_manager.create_inner_window("codex", command)
720
+ if not created:
721
+ self.app.notify("Failed to open Codex terminal", title="Terminal", severity="error")
722
+
723
+ async def _create_opencode_terminal(self, workspace: str) -> None:
724
+ """Create a new Opencode terminal."""
725
+ command = self._command_in_directory(
726
+ tmux_manager.zsh_run_and_keep_open("opencode"), workspace
727
+ )
728
+ created = tmux_manager.create_inner_window("opencode", command)
729
+ if not created:
730
+ self.app.notify("Failed to open Opencode terminal", title="Terminal", severity="error")
754
731
 
755
- if button_id == "new-codex":
756
- workspace = await self._select_workspace("Select workspace for Codex")
757
- if not workspace:
758
- return
732
+ async def _create_zsh_terminal(self, workspace: str) -> None:
733
+ """Create a new zsh terminal."""
734
+ command = self._command_in_directory("zsh", workspace)
735
+ created = tmux_manager.create_inner_window("zsh", command)
736
+ if not created:
737
+ self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
759
738
 
760
- command = self._command_in_directory(
761
- tmux_manager.zsh_run_and_keep_open("codex"), workspace
739
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
740
+ button_id = event.button.id or ""
741
+
742
+ if not self.supported():
743
+ self.app.notify(
744
+ self._support_message(),
745
+ title="Terminal",
746
+ severity="warning",
762
747
  )
763
- created = tmux_manager.create_inner_window("codex", command)
764
- if not created:
765
- self.app.notify("Failed to open codex terminal", title="Terminal", severity="error")
766
- await self.refresh_data()
767
748
  return
768
749
 
769
- if button_id == "new-zsh":
770
- created = tmux_manager.create_inner_window("zsh", "zsh")
771
- if not created:
772
- self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
773
- await self.refresh_data()
750
+ if button_id == "new-agent":
751
+ from ..screens import CreateAgentScreen
752
+
753
+ self.app.push_screen(CreateAgentScreen(), self._on_create_agent_result)
774
754
  return
775
755
 
776
756
  if button_id.startswith("switch-"):
@@ -389,7 +389,9 @@ class WatcherPanel(Container, can_focus=True):
389
389
  except Exception:
390
390
  return []
391
391
 
392
- def _collect_recent_sessions_page(self, *, page: int, rows_per_page: int) -> tuple[list[dict], int]:
392
+ def _collect_recent_sessions_page(
393
+ self, *, page: int, rows_per_page: int
394
+ ) -> tuple[list[dict], int]:
393
395
  """Collect one page of recent sessions from the database."""
394
396
  try:
395
397
  from ...db import get_database
@@ -477,7 +479,9 @@ class WatcherPanel(Container, can_focus=True):
477
479
  if exists:
478
480
  paths_lines.append(f" [green]●[/green] {name}: {path}")
479
481
  else:
480
- paths_lines.append(f" [yellow]○[/yellow] {name}: {path} [dim](not found)[/dim]")
482
+ paths_lines.append(
483
+ f" [yellow]○[/yellow] {name}: {path} [dim](not found)[/dim]"
484
+ )
481
485
  else:
482
486
  paths_lines.append(f" [dim]○ {name}: {path} (disabled)[/dim]")
483
487
  paths_widget.update("\n".join(paths_lines))
@@ -483,7 +483,16 @@ class WorkerPanel(Container, can_focus=True):
483
483
  ("Failed", failed, "red"),
484
484
  ]:
485
485
  bar_width = int((count / total) * max_width) if total > 0 else 0
486
- bar = "[" + color + "]" + ("█" * bar_width) + "[/" + color + "]" + ("░" * (max_width - bar_width))
486
+ bar = (
487
+ "["
488
+ + color
489
+ + "]"
490
+ + ("█" * bar_width)
491
+ + "[/"
492
+ + color
493
+ + "]"
494
+ + ("░" * (max_width - bar_width))
495
+ )
487
496
  lines.append(f" {label:<12} {bar} {count}")
488
497
 
489
498
  return "\n".join(lines)
realign/db/__init__.py CHANGED
@@ -20,7 +20,7 @@ def get_database(
20
20
  blocking CLI commands under worker/watcher write load.
21
21
  """
22
22
  global _DB_INSTANCE
23
-
23
+
24
24
  # Resolution order:
25
25
  # 1) Env override (tests/ops): REALIGN_SQLITE_DB_PATH or REALIGN_DB_PATH (legacy)
26
26
  # 2) Config: ~/.aline/config.yaml (sqlite_db_path)
realign/db/base.py CHANGED
@@ -121,9 +121,7 @@ class DatabaseInterface(ABC):
121
121
  pass
122
122
 
123
123
  @abstractmethod
124
- def get_or_create_project(
125
- self, path: Path, name: Optional[str] = None
126
- ) -> ProjectRecord:
124
+ def get_or_create_project(self, path: Path, name: Optional[str] = None) -> ProjectRecord:
127
125
  """Get existing project or create new one."""
128
126
  pass
129
127
 
@@ -141,9 +139,7 @@ class DatabaseInterface(ABC):
141
139
  pass
142
140
 
143
141
  @abstractmethod
144
- def update_session_activity(
145
- self, session_id: str, last_activity_at: datetime
146
- ) -> None:
142
+ def update_session_activity(self, session_id: str, last_activity_at: datetime) -> None:
147
143
  """Update last activity timestamp for a session."""
148
144
  pass
149
145
 
@@ -212,16 +208,12 @@ class DatabaseInterface(ABC):
212
208
  pass
213
209
 
214
210
  @abstractmethod
215
- def get_turn_by_hash(
216
- self, session_id: str, content_hash: str
217
- ) -> Optional[TurnRecord]:
211
+ def get_turn_by_hash(self, session_id: str, content_hash: str) -> Optional[TurnRecord]:
218
212
  """Check if a turn with this content hash already exists in the session."""
219
213
  pass
220
214
 
221
215
  @abstractmethod
222
- def get_turn_by_number(
223
- self, session_id: str, turn_number: int
224
- ) -> Optional[TurnRecord]:
216
+ def get_turn_by_number(self, session_id: str, turn_number: int) -> Optional[TurnRecord]:
225
217
  """Get a turn by session_id and turn_number."""
226
218
  pass
227
219
 
@@ -379,9 +371,7 @@ class DatabaseInterface(ABC):
379
371
  pass
380
372
 
381
373
  @abstractmethod
382
- def try_acquire_lock(
383
- self, lock_key: str, *, owner: str, ttl_seconds: float
384
- ) -> bool:
374
+ def try_acquire_lock(self, lock_key: str, *, owner: str, ttl_seconds: float) -> bool:
385
375
  """Try to acquire a cross-process lease lock."""
386
376
  pass
387
377
 
realign/db/locks.py CHANGED
@@ -65,4 +65,3 @@ def lease_lock(
65
65
  db.release_lock(lock_key, owner=lock_owner)
66
66
  except Exception:
67
67
  pass
68
-