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
@@ -0,0 +1,114 @@
1
+ """Help screen modal for the dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.binding import Binding
7
+ from textual.containers import Container, Vertical
8
+ from textual.screen import ModalScreen
9
+ from textual.widgets import Button, Static
10
+
11
+
12
+ class HelpScreen(ModalScreen):
13
+ """Modal showing keyboard shortcuts and help information."""
14
+
15
+ BINDINGS = [
16
+ Binding("escape", "close", "Close", show=False),
17
+ Binding("?", "close", "Close", show=False),
18
+ ]
19
+
20
+ DEFAULT_CSS = """
21
+ HelpScreen {
22
+ align: center middle;
23
+ }
24
+
25
+ HelpScreen #help-root {
26
+ width: 50;
27
+ height: auto;
28
+ max-height: 80%;
29
+ padding: 1 2;
30
+ background: $background;
31
+ border: solid $accent;
32
+ }
33
+
34
+ HelpScreen #help-title {
35
+ height: auto;
36
+ margin-bottom: 1;
37
+ text-style: bold;
38
+ text-align: center;
39
+ }
40
+
41
+ HelpScreen .shortcut-section {
42
+ height: auto;
43
+ margin-bottom: 1;
44
+ }
45
+
46
+ HelpScreen .section-title {
47
+ height: auto;
48
+ color: $text-muted;
49
+ margin-bottom: 0;
50
+ }
51
+
52
+ HelpScreen .shortcut-row {
53
+ height: auto;
54
+ padding: 0 1;
55
+ }
56
+
57
+ HelpScreen .shortcut-key {
58
+ width: 14;
59
+ height: auto;
60
+ color: $accent;
61
+ }
62
+
63
+ HelpScreen .shortcut-desc {
64
+ width: 1fr;
65
+ height: auto;
66
+ }
67
+
68
+ HelpScreen #close-btn {
69
+ margin-top: 1;
70
+ width: 100%;
71
+ }
72
+ """
73
+
74
+ def compose(self) -> ComposeResult:
75
+ with Container(id="help-root"):
76
+ yield Static("Keyboard Shortcuts", id="help-title")
77
+
78
+ with Vertical(classes="shortcut-section"):
79
+ yield Static("Navigation", classes="section-title")
80
+ yield self._shortcut_row("Tab", "Next tab")
81
+ yield self._shortcut_row("Shift+Tab", "Previous tab")
82
+ yield self._shortcut_row("n", "Next page")
83
+ yield self._shortcut_row("p", "Previous page")
84
+
85
+ with Vertical(classes="shortcut-section"):
86
+ yield Static("Actions", classes="section-title")
87
+ yield self._shortcut_row("Enter", "Open selected item")
88
+ yield self._shortcut_row("Space", "Toggle selection")
89
+ yield self._shortcut_row("c", "Create event")
90
+ yield self._shortcut_row("l", "Load context")
91
+ yield self._shortcut_row("y", "Import share")
92
+ yield self._shortcut_row("s", "Switch view")
93
+ yield self._shortcut_row("r", "Refresh")
94
+
95
+ with Vertical(classes="shortcut-section"):
96
+ yield Static("General", classes="section-title")
97
+ yield self._shortcut_row("?", "Show this help")
98
+ yield self._shortcut_row("Ctrl+C x2", "Quit")
99
+
100
+ yield Button("Close", id="close-btn", variant="primary")
101
+
102
+ def _shortcut_row(self, key: str, description: str) -> Static:
103
+ """Create a shortcut row with key and description."""
104
+ return Static(f"[bold $accent]{key:<12}[/] {description}", classes="shortcut-row")
105
+
106
+ def on_mount(self) -> None:
107
+ self.query_one("#close-btn", Button).focus()
108
+
109
+ def action_close(self) -> None:
110
+ self.app.pop_screen()
111
+
112
+ def on_button_pressed(self, event: Button.Pressed) -> None:
113
+ if event.button.id == "close-btn":
114
+ self.app.pop_screen()
@@ -162,7 +162,9 @@ class SessionDetailScreen(ModalScreen):
162
162
  session_title = getattr(self._session, "session_title", None) if self._session else None
163
163
  session_type = getattr(self._session, "session_type", None) if self._session else None
164
164
  workspace_path = getattr(self._session, "workspace_path", None) if self._session else None
165
- last_activity_at = getattr(self._session, "last_activity_at", None) if self._session else None
165
+ last_activity_at = (
166
+ getattr(self._session, "last_activity_at", None) if self._session else None
167
+ )
166
168
  started_at = getattr(self._session, "started_at", None) if self._session else None
167
169
 
168
170
  source_map = {
@@ -74,7 +74,9 @@ class ShareImportScreen(ModalScreen):
74
74
  placeholder="Paste share URL (e.g. https://.../share/abc123)",
75
75
  )
76
76
  with Horizontal(classes="row"):
77
- yield Input(id="share-password", placeholder="Password (optional)", password=True)
77
+ yield Input(
78
+ id="share-password", placeholder="Password (optional)", password=True
79
+ )
78
80
  with Horizontal(classes="row"):
79
81
  yield Checkbox("Force re-import (override duplicates)", id="share-force")
80
82
  with Horizontal(id="share-import-actions", classes="row"):
@@ -107,7 +109,10 @@ class ShareImportScreen(ModalScreen):
107
109
  self.query_one("#cancel", Button).disabled = busy
108
110
 
109
111
  def _start_import(self) -> None:
110
- if self._worker is not None and self._worker.state in (WorkerState.PENDING, WorkerState.RUNNING):
112
+ if self._worker is not None and self._worker.state in (
113
+ WorkerState.PENDING,
114
+ WorkerState.RUNNING,
115
+ ):
111
116
  return
112
117
 
113
118
  share_url = self.query_one("#share-url", Input).value.strip()
@@ -181,4 +186,3 @@ class ShareImportScreen(ModalScreen):
181
186
  message = f"{message}: {stderr_text}"
182
187
  status.update(f"[red]{message}[/red]")
183
188
  self.app.notify(message, title="Share Import", severity="error", timeout=6)
184
-
@@ -96,7 +96,9 @@ def _run_tmux(args: Sequence[str], *, capture: bool = False) -> subprocess.Compl
96
96
  )
97
97
 
98
98
 
99
- def _run_outer_tmux(args: Sequence[str], *, capture: bool = False) -> subprocess.CompletedProcess[str]:
99
+ def _run_outer_tmux(
100
+ args: Sequence[str], *, capture: bool = False
101
+ ) -> subprocess.CompletedProcess[str]:
100
102
  """Run tmux commands against the dedicated outer server socket."""
101
103
  return _run_tmux(["-L", OUTER_SOCKET, *args], capture=capture)
102
104
 
@@ -110,7 +112,11 @@ def _run_inner_tmux(
110
112
  def _python_dashboard_command() -> str:
111
113
  # Use the current interpreter for predictable environments (venv, editable installs).
112
114
  python_cmd = shlex.join(
113
- [sys.executable, "-c", "from realign.dashboard.app import run_dashboard; run_dashboard()"]
115
+ [
116
+ sys.executable,
117
+ "-c",
118
+ "from realign.dashboard.app import run_dashboard; run_dashboard()",
119
+ ]
114
120
  )
115
121
  return f"{MANAGED_ENV}=1 {python_cmd}"
116
122
 
@@ -209,7 +215,16 @@ def _aline_tmux_conf_path() -> Path:
209
215
  def _source_aline_tmux_config(run_fn) -> None: # type: ignore[no-untyped-def]
210
216
  """Best-effort source ~/.aline/tmux/tmux.conf if present."""
211
217
  try:
212
- conf = _aline_tmux_conf_path()
218
+ # Ensure the config exists and is parseable.
219
+ # Users may run `aline dashboard` before `aline init`, or have older auto-generated configs
220
+ # that included unquoted `#` bindings (tmux treats `#` as a comment delimiter).
221
+ try:
222
+ from ..commands.init import _initialize_tmux_config
223
+
224
+ conf = _initialize_tmux_config()
225
+ except Exception:
226
+ conf = _aline_tmux_conf_path()
227
+
213
228
  if conf.exists():
214
229
  run_fn(["source-file", str(conf)])
215
230
  except Exception:
@@ -290,7 +305,11 @@ def _maximize_terminal_window() -> None:
290
305
 
291
306
  if front_app == "Terminal":
292
307
  proc = subprocess.run(
293
- ["osascript", "-e", 'tell application "Terminal" to set zoomed of front window to true'],
308
+ [
309
+ "osascript",
310
+ "-e",
311
+ 'tell application "Terminal" to set zoomed of front window to true',
312
+ ],
294
313
  capture_output=True,
295
314
  text=True,
296
315
  timeout=2,
@@ -377,6 +396,17 @@ def bootstrap_dashboard_into_tmux() -> None:
377
396
  # Enable mouse for the managed session only.
378
397
  _run_outer_tmux(["set-option", "-t", OUTER_SESSION, "mouse", "on"])
379
398
 
399
+ # Disable status bar for cleaner UI (Aline sessions only).
400
+ _run_outer_tmux(["set-option", "-t", OUTER_SESSION, "status", "off"])
401
+
402
+ # Pane border styling - use double lines for wider, more visible borders.
403
+ # This helps users identify the resizable border area more easily and reduces
404
+ # accidental drag-to-resize when trying to select text near the border.
405
+ _run_outer_tmux(["set-option", "-t", OUTER_SESSION, "pane-border-lines", "double"])
406
+ _run_outer_tmux(["set-option", "-t", OUTER_SESSION, "pane-border-style", "fg=brightblack"])
407
+ _run_outer_tmux(["set-option", "-t", OUTER_SESSION, "pane-active-border-style", "fg=blue"])
408
+ _run_outer_tmux(["set-option", "-t", OUTER_SESSION, "pane-border-indicators", "arrows"])
409
+
380
410
  # Ensure dashboard window exists.
381
411
  windows_out = (
382
412
  _run_outer_tmux(
@@ -422,6 +452,18 @@ def ensure_inner_session() -> bool:
422
452
 
423
453
  # Dedicated inner server; safe to enable mouse globally there.
424
454
  _run_inner_tmux(["set-option", "-g", "mouse", "on"])
455
+
456
+ # Disable status bar for cleaner UI.
457
+ _run_inner_tmux(["set-option", "-t", INNER_SESSION, "status", "off"])
458
+
459
+ # Pane border styling - use double lines for wider, more visible borders.
460
+ # This helps users identify the resizable border area more easily and reduces
461
+ # accidental drag-to-resize when trying to select text near the border.
462
+ _run_inner_tmux(["set-option", "-g", "pane-border-lines", "double"])
463
+ _run_inner_tmux(["set-option", "-g", "pane-border-style", "fg=brightblack"])
464
+ _run_inner_tmux(["set-option", "-g", "pane-active-border-style", "fg=blue"])
465
+ _run_inner_tmux(["set-option", "-g", "pane-border-indicators", "arrows"])
466
+
425
467
  _source_aline_tmux_config(_run_inner_tmux)
426
468
  return True
427
469
 
@@ -436,7 +478,13 @@ def ensure_right_pane(width_percent: int = 50) -> bool:
436
478
 
437
479
  panes_out = (
438
480
  _run_tmux(
439
- ["list-panes", "-t", f"{OUTER_SESSION}:{OUTER_WINDOW}", "-F", "#{pane_index}"],
481
+ [
482
+ "list-panes",
483
+ "-t",
484
+ f"{OUTER_SESSION}:{OUTER_WINDOW}",
485
+ "-F",
486
+ "#{pane_index}",
487
+ ],
440
488
  capture=True,
441
489
  ).stdout
442
490
  or ""
@@ -647,10 +695,7 @@ def clear_attention(window_id: str) -> bool:
647
695
  """Clear the attention state for a window (e.g., after user acknowledges permission request)."""
648
696
  if not ensure_inner_session():
649
697
  return False
650
- return (
651
- _run_inner_tmux(["set-option", "-w", "-t", window_id, OPT_ATTENTION, ""]).returncode
652
- == 0
653
- )
698
+ return _run_inner_tmux(["set-option", "-w", "-t", window_id, OPT_ATTENTION, ""]).returncode == 0
654
699
 
655
700
 
656
701
  def get_active_claude_context_id() -> str | None:
@@ -6,7 +6,9 @@ from typing import Optional
6
6
 
7
7
  from textual.app import ComposeResult
8
8
  from textual.containers import Horizontal
9
- from textual.widgets import Button, DataTable, Input, Static
9
+ from textual.widgets import Button, DataTable, Input, Static, Switch
10
+
11
+ from ..tmux_manager import _run_outer_tmux, OUTER_SESSION
10
12
 
11
13
 
12
14
  class ConfigPanel(Static):
@@ -51,6 +53,27 @@ class ConfigPanel(Static):
51
53
  ConfigPanel .button-row Button {
52
54
  margin-right: 1;
53
55
  }
56
+
57
+ ConfigPanel .tmux-settings {
58
+ height: auto;
59
+ margin-top: 1;
60
+ padding: 1;
61
+ border: solid $secondary;
62
+ }
63
+
64
+ ConfigPanel .tmux-settings .setting-row {
65
+ height: 3;
66
+ align: left middle;
67
+ }
68
+
69
+ ConfigPanel .tmux-settings .setting-label {
70
+ width: auto;
71
+ margin-right: 1;
72
+ }
73
+
74
+ ConfigPanel .tmux-settings Switch {
75
+ width: auto;
76
+ }
54
77
  """
55
78
 
56
79
  def __init__(self) -> None:
@@ -58,6 +81,8 @@ class ConfigPanel(Static):
58
81
  self._config_path: Optional[Path] = None
59
82
  self._config_data: dict = {}
60
83
  self._selected_key: Optional[str] = None
84
+ self._border_resize_enabled: bool = True # Track tmux border resize state
85
+ self._syncing_switch: bool = False # Flag to prevent recursive switch updates
61
86
 
62
87
  def compose(self) -> ComposeResult:
63
88
  """Compose the config panel layout."""
@@ -71,6 +96,13 @@ class ConfigPanel(Static):
71
96
  yield Button("Save", id="save-btn", variant="primary")
72
97
  yield Button("Reload", id="reload-btn", variant="default")
73
98
 
99
+ # Tmux settings section
100
+ with Static(classes="tmux-settings"):
101
+ yield Static("[bold]Tmux Settings[/bold]", classes="section-title")
102
+ with Horizontal(classes="setting-row"):
103
+ yield Static("Allow border resize:", classes="setting-label")
104
+ yield Switch(value=True, id="border-resize-switch")
105
+
74
106
  def on_mount(self) -> None:
75
107
  """Set up the panel on mount."""
76
108
  table = self.query_one("#config-table", DataTable)
@@ -80,6 +112,9 @@ class ConfigPanel(Static):
80
112
  # Load initial data
81
113
  self.refresh_data()
82
114
 
115
+ # Query and set the actual tmux border resize state
116
+ self._sync_border_resize_switch()
117
+
83
118
  def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
84
119
  """Handle row selection in the config table."""
85
120
  table = self.query_one("#config-table", DataTable)
@@ -114,6 +149,55 @@ class ConfigPanel(Static):
114
149
  self.refresh_data()
115
150
  self.app.notify("Configuration reloaded", title="Config")
116
151
 
152
+ def on_switch_changed(self, event: Switch.Changed) -> None:
153
+ """Handle switch toggle events."""
154
+ if self._syncing_switch:
155
+ return # Ignore events during sync
156
+ if event.switch.id == "border-resize-switch":
157
+ self._toggle_border_resize(event.value)
158
+
159
+ def _sync_border_resize_switch(self) -> None:
160
+ """Query tmux state and sync the switch to match."""
161
+ try:
162
+ # Check if MouseDrag1Border is bound by listing keys
163
+ result = _run_outer_tmux(["list-keys", "-T", "root"], capture=True)
164
+ output = result.stdout or ""
165
+
166
+ # If MouseDrag1Border is in the output, resize is enabled
167
+ is_enabled = "MouseDrag1Border" in output
168
+ self._border_resize_enabled = is_enabled
169
+
170
+ # Update switch without triggering the toggle action
171
+ self._syncing_switch = True
172
+ try:
173
+ switch = self.query_one("#border-resize-switch", Switch)
174
+ switch.value = is_enabled
175
+ finally:
176
+ self._syncing_switch = False
177
+ except Exception:
178
+ # If we can't query, assume enabled (default tmux behavior)
179
+ pass
180
+
181
+ def _toggle_border_resize(self, enabled: bool) -> None:
182
+ """Enable or disable tmux border resize functionality."""
183
+ try:
184
+ if enabled:
185
+ # Re-enable border resize by binding MouseDrag1Border to default resize behavior
186
+ _run_outer_tmux([
187
+ "bind", "-n", "MouseDrag1Border", "resize-pane", "-M"
188
+ ])
189
+ self._border_resize_enabled = True
190
+ self.app.notify("Border resize enabled", title="Tmux")
191
+ else:
192
+ # Disable border resize by unbinding MouseDrag1Border
193
+ _run_outer_tmux([
194
+ "unbind", "-n", "MouseDrag1Border"
195
+ ])
196
+ self._border_resize_enabled = False
197
+ self.app.notify("Border resize disabled", title="Tmux")
198
+ except Exception as e:
199
+ self.app.notify(f"Error toggling border resize: {e}", title="Tmux", severity="error")
200
+
117
201
  def _save_config(self) -> None:
118
202
  """Save the edited configuration."""
119
203
  if not self._selected_key: