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.
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
- realign/__init__.py +1 -1
- realign/adapters/__init__.py +0 -3
- realign/adapters/codex.py +14 -9
- realign/cli.py +42 -236
- realign/codex_detector.py +72 -32
- realign/codex_home.py +85 -0
- realign/codex_terminal_linker.py +172 -0
- realign/commands/__init__.py +2 -2
- realign/commands/add.py +89 -9
- realign/commands/doctor.py +495 -0
- realign/commands/export_shares.py +154 -226
- realign/commands/init.py +66 -4
- realign/commands/watcher.py +30 -80
- realign/config.py +9 -46
- realign/dashboard/app.py +7 -11
- realign/dashboard/screens/event_detail.py +0 -3
- realign/dashboard/screens/session_detail.py +0 -1
- realign/dashboard/tmux_manager.py +129 -4
- realign/dashboard/widgets/config_panel.py +175 -241
- realign/dashboard/widgets/events_table.py +71 -128
- realign/dashboard/widgets/sessions_table.py +77 -136
- realign/dashboard/widgets/terminal_panel.py +349 -27
- realign/dashboard/widgets/watcher_panel.py +0 -2
- realign/db/sqlite_db.py +77 -2
- realign/events/event_summarizer.py +76 -35
- realign/events/session_summarizer.py +73 -32
- realign/hooks.py +334 -647
- realign/llm_client.py +201 -520
- realign/triggers/__init__.py +0 -2
- realign/triggers/next_turn_trigger.py +4 -5
- realign/triggers/registry.py +1 -4
- realign/watcher_core.py +53 -35
- realign/adapters/antigravity.py +0 -159
- realign/triggers/antigravity_trigger.py +0 -140
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
11
|
-
from textual.widgets import Button,
|
|
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
|
|
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
|
|
50
|
-
height:
|
|
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 .
|
|
62
|
-
|
|
45
|
+
ConfigPanel .button-row Button {
|
|
46
|
+
margin-right: 1;
|
|
63
47
|
}
|
|
64
48
|
|
|
65
|
-
ConfigPanel .
|
|
49
|
+
ConfigPanel .account-section {
|
|
66
50
|
height: 3;
|
|
67
|
-
|
|
51
|
+
align: left middle;
|
|
68
52
|
}
|
|
69
53
|
|
|
70
|
-
ConfigPanel .
|
|
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:
|
|
77
|
-
padding: 1;
|
|
78
|
-
border: solid $secondary;
|
|
66
|
+
margin-top: 2;
|
|
79
67
|
}
|
|
80
68
|
|
|
81
69
|
ConfigPanel .tmux-settings .setting-row {
|
|
82
|
-
height:
|
|
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
|
|
77
|
+
ConfigPanel .tmux-settings RadioSet {
|
|
92
78
|
width: auto;
|
|
79
|
+
height: auto;
|
|
80
|
+
layout: horizontal;
|
|
93
81
|
}
|
|
94
82
|
|
|
95
|
-
ConfigPanel .
|
|
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:
|
|
98
|
-
padding: 1;
|
|
99
|
-
border: solid $success;
|
|
90
|
+
margin-top: 2;
|
|
100
91
|
}
|
|
101
92
|
|
|
102
|
-
ConfigPanel .
|
|
103
|
-
|
|
93
|
+
ConfigPanel .terminal-settings {
|
|
94
|
+
height: auto;
|
|
95
|
+
margin-top: 2;
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
ConfigPanel .
|
|
107
|
-
|
|
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.
|
|
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
|
|
135
|
-
yield Static("[bold]Account[/bold]", classes="
|
|
136
|
-
yield Static(id="account-
|
|
137
|
-
|
|
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("
|
|
146
|
-
|
|
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.
|
|
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 == "
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
elif event.button.id == "
|
|
201
|
-
self.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
210
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
336
|
-
"""Query tmux state and sync the
|
|
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
|
|
347
|
-
self.
|
|
327
|
+
# Update radio buttons without triggering the toggle action
|
|
328
|
+
self._syncing_radio = True
|
|
348
329
|
try:
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
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
|
|
378
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
430
|
-
"""
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
422
|
+
thread = threading.Thread(target=do_doctor, daemon=True)
|
|
423
|
+
thread.start()
|
|
486
424
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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()
|