aline-ai 0.6.2__py3-none-any.whl → 0.6.3__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.
@@ -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,131 @@ 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;
93
- }
94
-
95
- ConfigPanel .account-section {
96
79
  height: auto;
97
- margin-top: 1;
98
- padding: 1;
99
- border: solid $success;
80
+ layout: horizontal;
100
81
  }
101
82
 
102
- ConfigPanel .account-section .account-status {
103
- margin-bottom: 1;
83
+ ConfigPanel .tmux-settings RadioButton {
84
+ width: auto;
85
+ margin-right: 2;
104
86
  }
105
87
 
106
- ConfigPanel .account-section Button {
107
- margin-right: 1;
88
+ ConfigPanel .tools-section {
89
+ height: auto;
90
+ margin-top: 2;
108
91
  }
109
92
  """
110
93
 
111
94
  def __init__(self) -> None:
112
95
  super().__init__()
113
- self._config_path: Optional[Path] = None
114
- self._config_data: dict = {}
115
- self._selected_key: Optional[str] = None
116
96
  self._border_resize_enabled: bool = True # Track tmux border resize state
117
- self._syncing_switch: bool = False # Flag to prevent recursive switch updates
97
+ self._syncing_radio: bool = False # Flag to prevent recursive radio updates
118
98
  self._login_in_progress: bool = False # Track login state
119
99
  self._refresh_timer = None # Timer for auto-refresh
120
100
 
121
101
  def compose(self) -> ComposeResult:
122
102
  """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
103
  # 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")
104
+ with Horizontal(classes="account-section"):
105
+ yield Static("[bold]Account:[/bold]", classes="account-label")
106
+ yield Static(id="account-email", classes="account-email")
107
+ yield Button("Login", id="auth-btn", variant="primary")
140
108
 
141
109
  # Tmux settings section
142
110
  with Static(classes="tmux-settings"):
143
111
  yield Static("[bold]Tmux Settings[/bold]", classes="section-title")
144
112
  with Horizontal(classes="setting-row"):
145
- yield Static("Allow border resize:", classes="setting-label")
146
- yield Switch(value=True, id="border-resize-switch")
113
+ yield Static("Border resize:", classes="setting-label")
114
+ with RadioSet(id="border-resize-radio"):
115
+ yield RadioButton("Enabled", id="border-resize-enabled", value=True)
116
+ yield RadioButton("Disabled", id="border-resize-disabled")
117
+
118
+ # Tools section
119
+ with Static(classes="tools-section"):
120
+ yield Static("[bold]Tools[/bold]", classes="section-title")
121
+ with Horizontal(classes="button-row"):
122
+ yield Button("Aline Doctor", id="doctor-btn", variant="default")
147
123
 
148
124
  def on_mount(self) -> None:
149
125
  """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
126
  # Update account status display
158
127
  self._update_account_status()
159
128
 
160
129
  # Query and set the actual tmux border resize state
161
- self._sync_border_resize_switch()
130
+ self._sync_border_resize_radio()
162
131
 
163
132
  # Start timer to periodically refresh account status (every 5 seconds)
164
133
  self._refresh_timer = self.set_interval(5.0, self._update_account_status)
165
134
 
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
135
  def on_button_pressed(self, event: Button.Pressed) -> None:
193
136
  """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:
137
+ if event.button.id == "auth-btn":
138
+ credentials = get_current_user()
139
+ if credentials:
140
+ self._handle_logout()
141
+ else:
142
+ self._handle_login()
143
+ elif event.button.id == "doctor-btn":
144
+ self._handle_doctor()
145
+
146
+ def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
147
+ """Handle radio set change events."""
148
+ if self._syncing_radio:
208
149
  return # Ignore events during sync
209
- if event.switch.id == "border-resize-switch":
210
- self._toggle_border_resize(event.value)
150
+ if event.radio_set.id == "border-resize-radio":
151
+ # Check which radio button is selected
152
+ enabled = event.pressed.id == "border-resize-enabled"
153
+ self._toggle_border_resize(enabled)
211
154
 
212
155
  def _update_account_status(self) -> None:
213
156
  """Update the account status display."""
214
157
  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)
158
+ email_widget = self.query_one("#account-email", Static)
159
+ auth_btn = self.query_one("#auth-btn", Button)
218
160
  except Exception:
219
161
  # Widget not ready yet
220
162
  return
@@ -225,15 +167,14 @@ class ConfigPanel(Static):
225
167
 
226
168
  credentials = get_current_user()
227
169
  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
170
+ email_widget.update(f"[bold]{credentials.email}[/bold]")
171
+ auth_btn.label = "Logout"
172
+ auth_btn.variant = "warning"
233
173
  else:
234
- status_widget.update("[yellow]Not logged in[/yellow]")
235
- login_btn.disabled = False
236
- logout_btn.disabled = True
174
+ email_widget.update("[dim]Not logged in[/dim]")
175
+ auth_btn.label = "Login"
176
+ auth_btn.variant = "primary"
177
+ auth_btn.disabled = False
237
178
 
238
179
  def _handle_login(self) -> None:
239
180
  """Handle login button click - start login flow in background."""
@@ -244,10 +185,10 @@ class ConfigPanel(Static):
244
185
  self._login_in_progress = True
245
186
 
246
187
  # 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]")
188
+ auth_btn = self.query_one("#auth-btn", Button)
189
+ auth_btn.disabled = True
190
+ email_widget = self.query_one("#account-email", Static)
191
+ email_widget.update("[cyan]Opening browser...[/cyan]")
251
192
 
252
193
  # Start login flow in background thread
253
194
  def do_login():
@@ -332,8 +273,8 @@ class ConfigPanel(Static):
332
273
  else:
333
274
  self.app.notify("Failed to logout", title="Account", severity="error")
334
275
 
335
- def _sync_border_resize_switch(self) -> None:
336
- """Query tmux state and sync the switch to match."""
276
+ def _sync_border_resize_radio(self) -> None:
277
+ """Query tmux state and sync the radio buttons to match."""
337
278
  try:
338
279
  # Check if MouseDrag1Border is bound by listing keys
339
280
  result = _run_outer_tmux(["list-keys", "-T", "root"], capture=True)
@@ -343,13 +284,16 @@ class ConfigPanel(Static):
343
284
  is_enabled = "MouseDrag1Border" in output
344
285
  self._border_resize_enabled = is_enabled
345
286
 
346
- # Update switch without triggering the toggle action
347
- self._syncing_switch = True
287
+ # Update radio buttons without triggering the toggle action
288
+ self._syncing_radio = True
348
289
  try:
349
- switch = self.query_one("#border-resize-switch", Switch)
350
- switch.value = is_enabled
290
+ if is_enabled:
291
+ radio = self.query_one("#border-resize-enabled", RadioButton)
292
+ else:
293
+ radio = self.query_one("#border-resize-disabled", RadioButton)
294
+ radio.value = True
351
295
  finally:
352
- self._syncing_switch = False
296
+ self._syncing_radio = False
353
297
  except Exception:
354
298
  # If we can't query, assume enabled (default tmux behavior)
355
299
  pass
@@ -374,120 +318,36 @@ class ConfigPanel(Static):
374
318
  except Exception as e:
375
319
  self.app.notify(f"Error toggling border resize: {e}", title="Tmux", severity="error")
376
320
 
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
321
+ def _handle_doctor(self) -> None:
322
+ """Run aline doctor command in background."""
323
+ self.app.notify("Running Aline Doctor...", title="Doctor")
382
324
 
383
- edit_input = self.query_one("#edit-input", Input)
384
- new_value = edit_input.value
385
-
386
- try:
387
- from ...config import ReAlignConfig
388
-
389
- 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",
325
+ def do_doctor():
326
+ try:
327
+ import subprocess
328
+ result = subprocess.run(
329
+ ["aline", "doctor"],
330
+ capture_output=True,
331
+ text=True,
332
+ timeout=60,
412
333
  )
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")
423
-
424
- def refresh_data(self) -> None:
425
- """Refresh configuration data."""
426
- self._load_config()
427
- self._update_display()
428
-
429
- def _load_config(self) -> None:
430
- """Load configuration from file."""
431
- try:
432
- from ...config import ReAlignConfig
433
-
434
- self._config_path = Path.home() / ".aline" / "config.yaml"
435
- config = ReAlignConfig.load()
436
-
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
450
-
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]"
334
+ if result.returncode == 0:
335
+ self.app.call_from_thread(
336
+ self.app.notify, "Doctor completed successfully", title="Doctor"
337
+ )
476
338
  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]"
339
+ error_msg = result.stderr.strip() if result.stderr else "Unknown error"
340
+ self.app.call_from_thread(
341
+ self.app.notify, f"Doctor failed: {error_msg}", title="Doctor", severity="error"
342
+ )
343
+ except Exception as e:
344
+ self.app.call_from_thread(
345
+ self.app.notify, f"Doctor error: {e}", title="Doctor", severity="error"
346
+ )
484
347
 
485
- table.add_row(key, value_display)
348
+ thread = threading.Thread(target=do_doctor, daemon=True)
349
+ thread.start()
486
350
 
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
351
+ def refresh_data(self) -> None:
352
+ """Refresh account status (called by app refresh action)."""
353
+ self._update_account_status()