aline-ai 0.6.2__py3-none-any.whl → 0.6.4__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 (40) hide show
  1. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
  3. realign/__init__.py +1 -1
  4. realign/adapters/__init__.py +0 -3
  5. realign/adapters/codex.py +14 -9
  6. realign/cli.py +42 -236
  7. realign/codex_detector.py +72 -32
  8. realign/codex_home.py +85 -0
  9. realign/codex_terminal_linker.py +172 -0
  10. realign/commands/__init__.py +2 -2
  11. realign/commands/add.py +89 -9
  12. realign/commands/doctor.py +495 -0
  13. realign/commands/export_shares.py +154 -226
  14. realign/commands/init.py +66 -4
  15. realign/commands/watcher.py +30 -80
  16. realign/config.py +9 -46
  17. realign/dashboard/app.py +7 -11
  18. realign/dashboard/screens/event_detail.py +0 -3
  19. realign/dashboard/screens/session_detail.py +0 -1
  20. realign/dashboard/tmux_manager.py +129 -4
  21. realign/dashboard/widgets/config_panel.py +175 -241
  22. realign/dashboard/widgets/events_table.py +71 -128
  23. realign/dashboard/widgets/sessions_table.py +77 -136
  24. realign/dashboard/widgets/terminal_panel.py +349 -27
  25. realign/dashboard/widgets/watcher_panel.py +0 -2
  26. realign/db/sqlite_db.py +77 -2
  27. realign/events/event_summarizer.py +76 -35
  28. realign/events/session_summarizer.py +73 -32
  29. realign/hooks.py +334 -647
  30. realign/llm_client.py +201 -520
  31. realign/triggers/__init__.py +0 -2
  32. realign/triggers/next_turn_trigger.py +4 -5
  33. realign/triggers/registry.py +1 -4
  34. realign/watcher_core.py +53 -35
  35. realign/adapters/antigravity.py +0 -159
  36. realign/triggers/antigravity_trigger.py +0 -140
  37. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
  38. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
  39. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
  40. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,17 @@
1
1
  """Config Panel Widget for viewing and editing configuration."""
2
2
 
3
3
  import threading
4
- import webbrowser
5
- from dataclasses import fields
6
- from pathlib import Path
7
- from typing import Optional
8
4
 
9
5
  from textual.app import ComposeResult
10
- from textual.containers import Horizontal, Vertical
11
- from textual.widgets import Button, DataTable, Input, Static, Switch
6
+ from textual.containers import Horizontal
7
+ from textual.widgets import Button, RadioButton, RadioSet, Static
12
8
 
13
- from ..tmux_manager import _run_outer_tmux, OUTER_SESSION
9
+ from ..tmux_manager import _run_outer_tmux
14
10
  from ...auth import (
15
11
  load_credentials,
16
12
  save_credentials,
17
13
  clear_credentials,
18
14
  open_login_page,
19
- is_logged_in,
20
15
  get_current_user,
21
16
  find_free_port,
22
17
  start_callback_server,
@@ -37,184 +32,171 @@ class ConfigPanel(Static):
37
32
  padding: 1;
38
33
  }
39
34
 
40
- ConfigPanel .config-path {
41
- margin-bottom: 1;
42
- }
43
-
44
35
  ConfigPanel .section-title {
45
36
  text-style: bold;
46
37
  margin-bottom: 1;
47
38
  }
48
39
 
49
- ConfigPanel DataTable {
50
- height: 1fr;
51
- max-height: 20;
52
- }
53
-
54
- ConfigPanel .edit-section {
55
- height: 5;
40
+ ConfigPanel .button-row {
41
+ height: 3;
56
42
  margin-top: 1;
57
- padding: 1;
58
- border: solid $primary;
59
43
  }
60
44
 
61
- ConfigPanel .edit-section Input {
62
- width: 1fr;
45
+ ConfigPanel .button-row Button {
46
+ margin-right: 1;
63
47
  }
64
48
 
65
- ConfigPanel .button-row {
49
+ ConfigPanel .account-section {
66
50
  height: 3;
67
- margin-top: 1;
51
+ align: left middle;
68
52
  }
69
53
 
70
- ConfigPanel .button-row Button {
54
+ ConfigPanel .account-section .account-label {
55
+ width: auto;
56
+ margin-right: 1;
57
+ }
58
+
59
+ ConfigPanel .account-section .account-email {
60
+ width: auto;
71
61
  margin-right: 1;
72
62
  }
73
63
 
74
64
  ConfigPanel .tmux-settings {
75
65
  height: auto;
76
- margin-top: 1;
77
- padding: 1;
78
- border: solid $secondary;
66
+ margin-top: 2;
79
67
  }
80
68
 
81
69
  ConfigPanel .tmux-settings .setting-row {
82
- height: 3;
83
- align: left middle;
70
+ height: auto;
84
71
  }
85
72
 
86
73
  ConfigPanel .tmux-settings .setting-label {
87
74
  width: auto;
88
- margin-right: 1;
89
75
  }
90
76
 
91
- ConfigPanel .tmux-settings Switch {
77
+ ConfigPanel .tmux-settings RadioSet {
92
78
  width: auto;
79
+ height: auto;
80
+ layout: horizontal;
93
81
  }
94
82
 
95
- ConfigPanel .account-section {
83
+ ConfigPanel .tmux-settings RadioButton {
84
+ width: auto;
85
+ margin-right: 2;
86
+ }
87
+
88
+ ConfigPanel .tools-section {
96
89
  height: auto;
97
- margin-top: 1;
98
- padding: 1;
99
- border: solid $success;
90
+ margin-top: 2;
100
91
  }
101
92
 
102
- ConfigPanel .account-section .account-status {
103
- margin-bottom: 1;
93
+ ConfigPanel .terminal-settings {
94
+ height: auto;
95
+ margin-top: 2;
104
96
  }
105
97
 
106
- ConfigPanel .account-section Button {
107
- margin-right: 1;
98
+ ConfigPanel .terminal-settings .setting-row {
99
+ height: auto;
100
+ }
101
+
102
+ ConfigPanel .terminal-settings .setting-label {
103
+ width: auto;
104
+ }
105
+
106
+ ConfigPanel .terminal-settings RadioSet {
107
+ width: auto;
108
+ height: auto;
109
+ layout: horizontal;
110
+ }
111
+
112
+ ConfigPanel .terminal-settings RadioButton {
113
+ width: auto;
114
+ margin-right: 2;
108
115
  }
109
116
  """
110
117
 
111
118
  def __init__(self) -> None:
112
119
  super().__init__()
113
- self._config_path: Optional[Path] = None
114
- self._config_data: dict = {}
115
- self._selected_key: Optional[str] = None
116
120
  self._border_resize_enabled: bool = True # Track tmux border resize state
117
- self._syncing_switch: bool = False # Flag to prevent recursive switch updates
121
+ self._syncing_radio: bool = False # Flag to prevent recursive radio updates
118
122
  self._login_in_progress: bool = False # Track login state
119
123
  self._refresh_timer = None # Timer for auto-refresh
124
+ self._auto_close_stale_enabled: bool = False # Track auto-close setting
120
125
 
121
126
  def compose(self) -> ComposeResult:
122
127
  """Compose the config panel layout."""
123
- yield Static(id="config-path", classes="config-path")
124
- yield Static("[bold]Configuration[/bold]", classes="section-title")
125
- yield DataTable(id="config-table")
126
- with Horizontal(classes="edit-section"):
127
- yield Static("Selected: ", id="selected-label")
128
- yield Input(id="edit-input", placeholder="Select a config item to edit...")
129
- with Horizontal(classes="button-row"):
130
- yield Button("Save", id="save-btn", variant="primary")
131
- yield Button("Reload", id="reload-btn", variant="default")
132
-
133
128
  # Account section
134
- with Static(classes="account-section"):
135
- yield Static("[bold]Account[/bold]", classes="section-title")
136
- yield Static(id="account-status", classes="account-status")
137
- with Horizontal(classes="button-row"):
138
- yield Button("Login", id="login-btn", variant="primary")
139
- yield Button("Logout", id="logout-btn", variant="warning")
129
+ with Horizontal(classes="account-section"):
130
+ yield Static("[bold]Account:[/bold]", classes="account-label")
131
+ yield Static(id="account-email", classes="account-email")
132
+ yield Button("Login", id="auth-btn", variant="primary")
140
133
 
141
134
  # Tmux settings section
142
135
  with Static(classes="tmux-settings"):
143
136
  yield Static("[bold]Tmux Settings[/bold]", classes="section-title")
144
137
  with Horizontal(classes="setting-row"):
145
- yield Static("Allow border resize:", classes="setting-label")
146
- yield Switch(value=True, id="border-resize-switch")
138
+ yield Static("Border resize:", classes="setting-label")
139
+ with RadioSet(id="border-resize-radio"):
140
+ yield RadioButton("Enabled", id="border-resize-enabled", value=True)
141
+ yield RadioButton("Disabled", id="border-resize-disabled")
142
+
143
+ # Terminal settings section
144
+ with Static(classes="terminal-settings"):
145
+ yield Static("[bold]Terminal Settings[/bold]", classes="section-title")
146
+ with Horizontal(classes="setting-row"):
147
+ yield Static("Auto-close stale terminals (24h):", classes="setting-label")
148
+ with RadioSet(id="auto-close-stale-radio"):
149
+ yield RadioButton("Enabled", id="auto-close-stale-enabled")
150
+ yield RadioButton("Disabled", id="auto-close-stale-disabled", value=True)
151
+
152
+ # Tools section
153
+ with Static(classes="tools-section"):
154
+ yield Static("[bold]Tools[/bold]", classes="section-title")
155
+ with Horizontal(classes="button-row"):
156
+ yield Button("Aline Doctor", id="doctor-btn", variant="default")
147
157
 
148
158
  def on_mount(self) -> None:
149
159
  """Set up the panel on mount."""
150
- table = self.query_one("#config-table", DataTable)
151
- table.add_columns("Setting", "Value")
152
- table.cursor_type = "row"
153
-
154
- # Load initial data
155
- self.refresh_data()
156
-
157
160
  # Update account status display
158
161
  self._update_account_status()
159
162
 
160
163
  # Query and set the actual tmux border resize state
161
- self._sync_border_resize_switch()
164
+ self._sync_border_resize_radio()
165
+
166
+ # Sync auto-close stale terminals setting from config
167
+ self._sync_auto_close_stale_radio()
162
168
 
163
169
  # Start timer to periodically refresh account status (every 5 seconds)
164
170
  self._refresh_timer = self.set_interval(5.0, self._update_account_status)
165
171
 
166
- def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
167
- """Handle row selection in the config table."""
168
- table = self.query_one("#config-table", DataTable)
169
-
170
- # Get the selected row data
171
- row_key = event.row_key
172
- if row_key is not None:
173
- row_data = table.get_row(row_key)
174
- if row_data and len(row_data) >= 2:
175
- key = str(row_data[0])
176
- value = str(row_data[1])
177
-
178
- self._selected_key = key
179
-
180
- # Update the edit section
181
- selected_label = self.query_one("#selected-label", Static)
182
- selected_label.update(f"Selected: [bold]{key}[/bold]")
183
-
184
- edit_input = self.query_one("#edit-input", Input)
185
- # Don't show masked values in input
186
- if "api_key" in key and value.endswith("..."):
187
- edit_input.value = ""
188
- edit_input.placeholder = "(enter new API key)"
189
- else:
190
- edit_input.value = value
191
-
192
172
  def on_button_pressed(self, event: Button.Pressed) -> None:
193
173
  """Handle button clicks."""
194
- if event.button.id == "save-btn":
195
- self._save_config()
196
- elif event.button.id == "reload-btn":
197
- self.refresh_data()
198
- self._update_account_status()
199
- self.app.notify("Configuration reloaded", title="Config")
200
- elif event.button.id == "login-btn":
201
- self._handle_login()
202
- elif event.button.id == "logout-btn":
203
- self._handle_logout()
204
-
205
- def on_switch_changed(self, event: Switch.Changed) -> None:
206
- """Handle switch toggle events."""
207
- if self._syncing_switch:
174
+ if event.button.id == "auth-btn":
175
+ credentials = get_current_user()
176
+ if credentials:
177
+ self._handle_logout()
178
+ else:
179
+ self._handle_login()
180
+ elif event.button.id == "doctor-btn":
181
+ self._handle_doctor()
182
+
183
+ def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
184
+ """Handle radio set change events."""
185
+ if self._syncing_radio:
208
186
  return # Ignore events during sync
209
- if event.switch.id == "border-resize-switch":
210
- self._toggle_border_resize(event.value)
187
+ if event.radio_set.id == "border-resize-radio":
188
+ # Check which radio button is selected
189
+ enabled = event.pressed.id == "border-resize-enabled"
190
+ self._toggle_border_resize(enabled)
191
+ elif event.radio_set.id == "auto-close-stale-radio":
192
+ enabled = event.pressed.id == "auto-close-stale-enabled"
193
+ self._toggle_auto_close_stale(enabled)
211
194
 
212
195
  def _update_account_status(self) -> None:
213
196
  """Update the account status display."""
214
197
  try:
215
- status_widget = self.query_one("#account-status", Static)
216
- login_btn = self.query_one("#login-btn", Button)
217
- logout_btn = self.query_one("#logout-btn", Button)
198
+ email_widget = self.query_one("#account-email", Static)
199
+ auth_btn = self.query_one("#auth-btn", Button)
218
200
  except Exception:
219
201
  # Widget not ready yet
220
202
  return
@@ -225,15 +207,14 @@ class ConfigPanel(Static):
225
207
 
226
208
  credentials = get_current_user()
227
209
  if credentials:
228
- status_widget.update(
229
- f"[green]Logged in as:[/green] [bold]{credentials.email}[/bold]"
230
- )
231
- login_btn.disabled = True
232
- logout_btn.disabled = False
210
+ email_widget.update(f"[bold]{credentials.email}[/bold]")
211
+ auth_btn.label = "Logout"
212
+ auth_btn.variant = "warning"
233
213
  else:
234
- status_widget.update("[yellow]Not logged in[/yellow]")
235
- login_btn.disabled = False
236
- logout_btn.disabled = True
214
+ email_widget.update("[dim]Not logged in[/dim]")
215
+ auth_btn.label = "Login"
216
+ auth_btn.variant = "primary"
217
+ auth_btn.disabled = False
237
218
 
238
219
  def _handle_login(self) -> None:
239
220
  """Handle login button click - start login flow in background."""
@@ -244,10 +225,10 @@ class ConfigPanel(Static):
244
225
  self._login_in_progress = True
245
226
 
246
227
  # Update UI to show login in progress
247
- login_btn = self.query_one("#login-btn", Button)
248
- login_btn.disabled = True
249
- status_widget = self.query_one("#account-status", Static)
250
- status_widget.update("[cyan]Opening browser for login...[/cyan]")
228
+ auth_btn = self.query_one("#auth-btn", Button)
229
+ auth_btn.disabled = True
230
+ email_widget = self.query_one("#account-email", Static)
231
+ email_widget.update("[cyan]Opening browser...[/cyan]")
251
232
 
252
233
  # Start login flow in background thread
253
234
  def do_login():
@@ -332,8 +313,8 @@ class ConfigPanel(Static):
332
313
  else:
333
314
  self.app.notify("Failed to logout", title="Account", severity="error")
334
315
 
335
- def _sync_border_resize_switch(self) -> None:
336
- """Query tmux state and sync the switch to match."""
316
+ def _sync_border_resize_radio(self) -> None:
317
+ """Query tmux state and sync the radio buttons to match."""
337
318
  try:
338
319
  # Check if MouseDrag1Border is bound by listing keys
339
320
  result = _run_outer_tmux(["list-keys", "-T", "root"], capture=True)
@@ -343,13 +324,16 @@ class ConfigPanel(Static):
343
324
  is_enabled = "MouseDrag1Border" in output
344
325
  self._border_resize_enabled = is_enabled
345
326
 
346
- # Update switch without triggering the toggle action
347
- self._syncing_switch = True
327
+ # Update radio buttons without triggering the toggle action
328
+ self._syncing_radio = True
348
329
  try:
349
- switch = self.query_one("#border-resize-switch", Switch)
350
- switch.value = is_enabled
330
+ if is_enabled:
331
+ radio = self.query_one("#border-resize-enabled", RadioButton)
332
+ else:
333
+ radio = self.query_one("#border-resize-disabled", RadioButton)
334
+ radio.value = True
351
335
  finally:
352
- self._syncing_switch = False
336
+ self._syncing_radio = False
353
337
  except Exception:
354
338
  # If we can't query, assume enabled (default tmux behavior)
355
339
  pass
@@ -374,120 +358,70 @@ class ConfigPanel(Static):
374
358
  except Exception as e:
375
359
  self.app.notify(f"Error toggling border resize: {e}", title="Tmux", severity="error")
376
360
 
377
- def _save_config(self) -> None:
378
- """Save the edited configuration."""
379
- if not self._selected_key:
380
- self.app.notify("No config item selected", title="Config", severity="warning")
381
- return
382
-
383
- edit_input = self.query_one("#edit-input", Input)
384
- new_value = edit_input.value
385
-
361
+ def _sync_auto_close_stale_radio(self) -> None:
362
+ """Sync radio buttons with config file setting."""
386
363
  try:
387
- from ...config import ReAlignConfig
388
-
389
364
  config = ReAlignConfig.load()
390
- key = self._selected_key
391
-
392
- # Validate and convert value
393
- if not hasattr(config, key):
394
- self.app.notify(f"Unknown config key: {key}", title="Config", severity="error")
395
- return
396
-
397
- # Type conversion
398
- field_type = ReAlignConfig.__annotations__.get(key)
399
- if field_type is int:
400
- new_value = int(new_value)
401
- elif field_type is float:
402
- new_value = float(new_value)
403
- elif field_type is bool:
404
- new_value = new_value.lower() in ("true", "1", "yes")
405
-
406
- # Special validation for llm_provider
407
- if key == "llm_provider" and new_value not in ("auto", "claude", "openai"):
408
- self.app.notify(
409
- "Invalid llm_provider value. Use: auto, claude, openai",
410
- title="Config",
411
- severity="error",
412
- )
413
- return
414
-
415
- setattr(config, key, new_value)
416
- config.save()
417
-
418
- self.app.notify(f"Saved: {key}", title="Config")
419
- self.refresh_data()
420
-
421
- except Exception as e:
422
- self.app.notify(f"Error saving config: {e}", title="Config", severity="error")
365
+ is_enabled = config.auto_close_stale_terminals
366
+ self._auto_close_stale_enabled = is_enabled
423
367
 
424
- def refresh_data(self) -> None:
425
- """Refresh configuration data."""
426
- self._load_config()
427
- self._update_display()
368
+ # Update radio buttons without triggering the toggle action
369
+ self._syncing_radio = True
370
+ try:
371
+ if is_enabled:
372
+ radio = self.query_one("#auto-close-stale-enabled", RadioButton)
373
+ else:
374
+ radio = self.query_one("#auto-close-stale-disabled", RadioButton)
375
+ radio.value = True
376
+ finally:
377
+ self._syncing_radio = False
378
+ except Exception:
379
+ pass
428
380
 
429
- def _load_config(self) -> None:
430
- """Load configuration from file."""
381
+ def _toggle_auto_close_stale(self, enabled: bool) -> None:
382
+ """Enable or disable auto-close stale terminals setting."""
431
383
  try:
432
- from ...config import ReAlignConfig
433
-
434
- self._config_path = Path.home() / ".aline" / "config.yaml"
435
384
  config = ReAlignConfig.load()
385
+ config.auto_close_stale_terminals = enabled
386
+ config.save()
387
+ self._auto_close_stale_enabled = enabled
388
+ if enabled:
389
+ self.app.notify("Auto-close stale terminals enabled", title="Terminal")
390
+ else:
391
+ self.app.notify("Auto-close stale terminals disabled", title="Terminal")
392
+ except Exception as e:
393
+ self.app.notify(f"Error saving setting: {e}", title="Config", severity="error")
436
394
 
437
- self._config_data = {}
438
- for field in fields(ReAlignConfig):
439
- key = field.name
440
- value = getattr(config, key)
441
-
442
- # Mask API keys for display
443
- if "api_key" in key and value:
444
- if len(str(value)) > 8:
445
- value = str(value)[:4] + "..." + str(value)[-4:]
446
- else:
447
- value = "***"
448
-
449
- self._config_data[key] = value
395
+ def _handle_doctor(self) -> None:
396
+ """Run aline doctor command in background."""
397
+ self.app.notify("Running Aline Doctor...", title="Doctor")
450
398
 
451
- except Exception as e:
452
- self._config_data = {"error": str(e)}
453
-
454
- def _update_display(self) -> None:
455
- """Update the display with current data."""
456
- # Update config path
457
- path_widget = self.query_one("#config-path", Static)
458
- if self._config_path:
459
- path_widget.update(f"[bold]Config file:[/bold] {self._config_path}")
460
- else:
461
- path_widget.update("[bold]Config file:[/bold] (not found)")
462
-
463
- # Update table
464
- table = self.query_one("#config-table", DataTable)
465
- table.clear()
466
-
467
- for key, value in self._config_data.items():
468
- # Color-code certain values
469
- if key == "llm_provider":
470
- if value == "auto":
471
- value_display = "[cyan]auto[/cyan]"
472
- elif value == "claude":
473
- value_display = "[yellow]claude[/yellow]"
474
- elif value == "openai":
475
- value_display = "[green]openai[/green]"
399
+ def do_doctor():
400
+ try:
401
+ import subprocess
402
+ result = subprocess.run(
403
+ ["aline", "doctor"],
404
+ capture_output=True,
405
+ text=True,
406
+ timeout=60,
407
+ )
408
+ if result.returncode == 0:
409
+ self.app.call_from_thread(
410
+ self.app.notify, "Doctor completed successfully", title="Doctor"
411
+ )
476
412
  else:
477
- value_display = str(value)
478
- elif isinstance(value, bool):
479
- value_display = "[green]true[/green]" if value else "[red]false[/red]"
480
- elif "api_key" in key and value and value != "None":
481
- value_display = f"[dim]{value}[/dim]"
482
- else:
483
- value_display = str(value) if value is not None else "[dim]None[/dim]"
413
+ error_msg = result.stderr.strip() if result.stderr else "Unknown error"
414
+ self.app.call_from_thread(
415
+ self.app.notify, f"Doctor failed: {error_msg}", title="Doctor", severity="error"
416
+ )
417
+ except Exception as e:
418
+ self.app.call_from_thread(
419
+ self.app.notify, f"Doctor error: {e}", title="Doctor", severity="error"
420
+ )
484
421
 
485
- table.add_row(key, value_display)
422
+ thread = threading.Thread(target=do_doctor, daemon=True)
423
+ thread.start()
486
424
 
487
- # Reset edit section
488
- selected_label = self.query_one("#selected-label", Static)
489
- selected_label.update("Selected: (none)")
490
- edit_input = self.query_one("#edit-input", Input)
491
- edit_input.value = ""
492
- edit_input.placeholder = "Select a config item to edit..."
493
- self._selected_key = None
425
+ def refresh_data(self) -> None:
426
+ """Refresh account status (called by app refresh action)."""
427
+ self._update_account_status()