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.
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/METADATA +1 -1
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/RECORD +28 -30
- realign/__init__.py +1 -1
- realign/adapters/__init__.py +0 -3
- realign/cli.py +0 -1
- realign/commands/export_shares.py +154 -226
- realign/commands/watcher.py +28 -79
- realign/config.py +1 -47
- realign/dashboard/app.py +2 -8
- realign/dashboard/screens/event_detail.py +0 -3
- realign/dashboard/screens/session_detail.py +0 -1
- realign/dashboard/widgets/config_panel.py +109 -249
- realign/dashboard/widgets/events_table.py +71 -128
- realign/dashboard/widgets/sessions_table.py +76 -135
- realign/dashboard/widgets/watcher_panel.py +0 -2
- realign/db/sqlite_db.py +1 -2
- realign/events/event_summarizer.py +76 -35
- realign/events/session_summarizer.py +73 -32
- realign/hooks.py +383 -574
- 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 +3 -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.3.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.3.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,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
|
|
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;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
ConfigPanel .account-section {
|
|
96
79
|
height: auto;
|
|
97
|
-
|
|
98
|
-
padding: 1;
|
|
99
|
-
border: solid $success;
|
|
80
|
+
layout: horizontal;
|
|
100
81
|
}
|
|
101
82
|
|
|
102
|
-
ConfigPanel .
|
|
103
|
-
|
|
83
|
+
ConfigPanel .tmux-settings RadioButton {
|
|
84
|
+
width: auto;
|
|
85
|
+
margin-right: 2;
|
|
104
86
|
}
|
|
105
87
|
|
|
106
|
-
ConfigPanel .
|
|
107
|
-
|
|
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.
|
|
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
|
|
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")
|
|
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("
|
|
146
|
-
|
|
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.
|
|
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 == "
|
|
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:
|
|
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.
|
|
210
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
336
|
-
"""Query tmux state and sync the
|
|
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
|
|
347
|
-
self.
|
|
287
|
+
# Update radio buttons without triggering the toggle action
|
|
288
|
+
self._syncing_radio = True
|
|
348
289
|
try:
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
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
|
|
378
|
-
"""
|
|
379
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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",
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
348
|
+
thread = threading.Thread(target=do_doctor, daemon=True)
|
|
349
|
+
thread.start()
|
|
486
350
|
|
|
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
|
|
351
|
+
def refresh_data(self) -> None:
|
|
352
|
+
"""Refresh account status (called by app refresh action)."""
|
|
353
|
+
self._update_account_status()
|