aline-ai 0.6.4__py3-none-any.whl → 0.6.5__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.4.dist-info → aline_ai-0.6.5.dist-info}/METADATA +1 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.5.dist-info}/RECORD +12 -12
- realign/__init__.py +1 -1
- realign/commands/doctor.py +3 -1
- realign/dashboard/app.py +2 -151
- realign/dashboard/tmux_manager.py +49 -8
- realign/dashboard/widgets/config_panel.py +17 -11
- realign/dashboard/widgets/terminal_panel.py +69 -26
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.5.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.5.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.5.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.5.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
aline_ai-0.6.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.6.5.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=02FiDcPQx1TGbGJO98rtDO7k-JAA9WrZKtygoavnEY8,1623
|
|
3
3
|
realign/auth.py,sha256=d_1yvCwluN5iIrdgjtuSKpOYAksDzrzNgntKacLVJrw,16583
|
|
4
4
|
realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
|
|
5
5
|
realign/cli.py,sha256=HZ_1Rm50z1oszCwvPAZcAdPt0Gl-dj0S0NMLy2sWu_4,35665
|
|
@@ -38,7 +38,7 @@ realign/commands/add.py,sha256=_Xzt9P15mwndA3JvBBVrki8tn9Cc0UP6SiLwM4RS8Nc,27232
|
|
|
38
38
|
realign/commands/auth.py,sha256=QrPukpP-ogYEDSwztV0NOYI-HDgn5fPxlCQ1-e2n7gU,11082
|
|
39
39
|
realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,6732
|
|
40
40
|
realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
|
|
41
|
-
realign/commands/doctor.py,sha256=
|
|
41
|
+
realign/commands/doctor.py,sha256=q5UOrUR5Uai4AxgaeOnK1Hig5I5UX7m3Vt00tPnUllg,18289
|
|
42
42
|
realign/commands/export_shares.py,sha256=WNOR7FBE2om9qPO_28edZKhs94lyUAcbRgP_kNaDi5M,132574
|
|
43
43
|
realign/commands/import_shares.py,sha256=HiswLlYHqR0dR3wgB7Rs54_WownqahIs5IdyJOHuot8,25572
|
|
44
44
|
realign/commands/init.py,sha256=6rBr1LVIrQLbUH_UvoDhkF1qXmMh2xkjNWCYAUz5Tho,35274
|
|
@@ -48,10 +48,10 @@ realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,1
|
|
|
48
48
|
realign/commands/watcher.py,sha256=4WTThIgr-Z5guKh_JqGDcPmerr97XiHrVaaijmckHsA,134350
|
|
49
49
|
realign/commands/worker.py,sha256=jTu7Pj60nTnn7SsH3oNCNnO6zl4TIFCJVNSC1OoQ_0o,23363
|
|
50
50
|
realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
|
|
51
|
-
realign/dashboard/app.py,sha256=
|
|
51
|
+
realign/dashboard/app.py,sha256=aB1pvuJu-qJ94UqNegB4lvIxUzQJovuC82WQjFnQIFc,10464
|
|
52
52
|
realign/dashboard/layout.py,sha256=sZxmFj6QTbkois9MHTvBEMMcnaRVehCDqugdbiFx10k,9072
|
|
53
53
|
realign/dashboard/terminal_backend.py,sha256=MlDfwtqhftyQK6jDNizQGFjAWIo5Bx2TDpSnP3MCZVM,3375
|
|
54
|
-
realign/dashboard/tmux_manager.py,sha256=
|
|
54
|
+
realign/dashboard/tmux_manager.py,sha256=Fc6OQbnOO4YV47BnrIkcr0SHnQuSFwUSqhepNkpqKLs,32942
|
|
55
55
|
realign/dashboard/backends/__init__.py,sha256=POROX7YKtukYZcLB1pi_kO0sSEpuO3y-hwmF3WIN1Kk,163
|
|
56
56
|
realign/dashboard/backends/iterm2.py,sha256=XYYJT5lrrp4pW_MyEqPZYkRI0qyKUwJlezwMidgnsHc,21390
|
|
57
57
|
realign/dashboard/backends/kitty.py,sha256=5jdkR1f2PwB8a4SnS3EG6uOQ2XU-PB7-cpKBfIJq3hU,12066
|
|
@@ -64,13 +64,13 @@ realign/dashboard/screens/session_detail.py,sha256=TBkHqSHyMxsLB2QdZq9m1EoiH8oRV
|
|
|
64
64
|
realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
|
|
65
65
|
realign/dashboard/styles/dashboard.tcss,sha256=ewonevBGLN-dfSsgxUk4VBCPchtxY4rx_vj1u6Ox2Fw,3454
|
|
66
66
|
realign/dashboard/widgets/__init__.py,sha256=3Pf2_K9obrertgv_psfxradgkI9RXlmjoXYQH7oBKm0,583
|
|
67
|
-
realign/dashboard/widgets/config_panel.py,sha256=
|
|
67
|
+
realign/dashboard/widgets/config_panel.py,sha256=eRJRuqImQ8eJIKCEj4O8EvYxI-ht_anrcYbT5JskWyU,15972
|
|
68
68
|
realign/dashboard/widgets/events_table.py,sha256=MKB1G1_xdQCujEhmMz_GKI4hs-PeEiqGEAH7Y3ZGanE,30852
|
|
69
69
|
realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
|
|
70
70
|
realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
|
|
71
71
|
realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
|
|
72
72
|
realign/dashboard/widgets/sessions_table.py,sha256=oMkYhQ55pUGOGYxEXM5P37mpGYA350BK8Rb8fVq9AS4,34008
|
|
73
|
-
realign/dashboard/widgets/terminal_panel.py,sha256=
|
|
73
|
+
realign/dashboard/widgets/terminal_panel.py,sha256=8WX2_EewlyFlxJYokw2akEqkJUjNt_-F8tzE7St3084,60132
|
|
74
74
|
realign/dashboard/widgets/watcher_panel.py,sha256=emVY1-aot9Dnf5UI9yyNeEmp4d2Gb-lrC28DjkeLjKA,19575
|
|
75
75
|
realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
|
|
76
76
|
realign/db/__init__.py,sha256=65LsNdsq_rkwNC1eg1OAr3HC0ORXtelOh0I8MhNGr-g,3288
|
|
@@ -97,8 +97,8 @@ realign/triggers/next_turn_trigger.py,sha256=-x80_I-WmIjXXzQHEPBykgx_GQW6oKaLDQx
|
|
|
97
97
|
realign/triggers/registry.py,sha256=dkIjSd8Bg-hF0nxaO2Fi2K-0Zipqv6vVjc-HYSrA_fY,3656
|
|
98
98
|
realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
|
|
99
99
|
realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
|
|
100
|
-
aline_ai-0.6.
|
|
101
|
-
aline_ai-0.6.
|
|
102
|
-
aline_ai-0.6.
|
|
103
|
-
aline_ai-0.6.
|
|
104
|
-
aline_ai-0.6.
|
|
100
|
+
aline_ai-0.6.5.dist-info/METADATA,sha256=RmD0VjSn_0nGStyFKgkNYUL3i2foaqg8UWUT3kOUTOc,1597
|
|
101
|
+
aline_ai-0.6.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
102
|
+
aline_ai-0.6.5.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
103
|
+
aline_ai-0.6.5.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
104
|
+
aline_ai-0.6.5.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
realign/commands/doctor.py
CHANGED
|
@@ -308,6 +308,7 @@ def run_doctor(
|
|
|
308
308
|
start_if_not_running: bool,
|
|
309
309
|
verbose: bool,
|
|
310
310
|
clear_cache: bool,
|
|
311
|
+
auto_fix: bool = False,
|
|
311
312
|
) -> int:
|
|
312
313
|
"""
|
|
313
314
|
Core doctor logic.
|
|
@@ -317,6 +318,7 @@ def run_doctor(
|
|
|
317
318
|
start_if_not_running: If True and restart_daemons is True, start daemons even if not running.
|
|
318
319
|
verbose: Print details.
|
|
319
320
|
clear_cache: Clear Python bytecode cache for the installed package directory.
|
|
321
|
+
auto_fix: If True, automatically fix failed jobs without prompting.
|
|
320
322
|
"""
|
|
321
323
|
from ..auth import is_logged_in
|
|
322
324
|
from . import watcher as watcher_cmd
|
|
@@ -417,7 +419,7 @@ def run_doctor(
|
|
|
417
419
|
console.print(f" [yellow]![/yellow] Found {llm_error_count} turn(s) with LLM API errors")
|
|
418
420
|
|
|
419
421
|
# Ask user if they want to fix
|
|
420
|
-
if typer.confirm("\n Do you want to requeue these for regeneration?", default=True):
|
|
422
|
+
if auto_fix or typer.confirm("\n Do you want to requeue these for regeneration?", default=True):
|
|
421
423
|
requeued_jobs = 0
|
|
422
424
|
requeued_turns = 0
|
|
423
425
|
|
realign/dashboard/app.py
CHANGED
|
@@ -5,7 +5,6 @@ import subprocess
|
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
7
|
import traceback
|
|
8
|
-
from pathlib import Path
|
|
9
8
|
|
|
10
9
|
from textual.app import App, ComposeResult
|
|
11
10
|
from textual.binding import Binding
|
|
@@ -16,10 +15,7 @@ from .widgets import (
|
|
|
16
15
|
AlineHeader,
|
|
17
16
|
WatcherPanel,
|
|
18
17
|
WorkerPanel,
|
|
19
|
-
SessionsTable,
|
|
20
|
-
EventsTable,
|
|
21
18
|
ConfigPanel,
|
|
22
|
-
SearchPanel,
|
|
23
19
|
TerminalPanel,
|
|
24
20
|
)
|
|
25
21
|
|
|
@@ -71,9 +67,6 @@ class AlineDashboard(App):
|
|
|
71
67
|
Binding("n", "page_next", "Next Page", show=False),
|
|
72
68
|
Binding("p", "page_prev", "Prev Page", show=False),
|
|
73
69
|
Binding("s", "switch_view", "Switch View", show=False),
|
|
74
|
-
Binding("c", "create_event", "Create Event", show=False),
|
|
75
|
-
Binding("l", "load_context", "Load Context", show=False),
|
|
76
|
-
Binding("y", "share_import", "Share Import", show=False),
|
|
77
70
|
Binding("ctrl+c", "quit_confirm", "Quit", priority=True),
|
|
78
71
|
]
|
|
79
72
|
|
|
@@ -119,14 +112,8 @@ class AlineDashboard(App):
|
|
|
119
112
|
yield WatcherPanel()
|
|
120
113
|
with TabPane("Worker", id="worker"):
|
|
121
114
|
yield WorkerPanel()
|
|
122
|
-
with TabPane("Contexts", id="sessions"):
|
|
123
|
-
yield SessionsTable()
|
|
124
|
-
with TabPane("Share", id="events"):
|
|
125
|
-
yield EventsTable()
|
|
126
115
|
with TabPane("Config", id="config"):
|
|
127
116
|
yield ConfigPanel()
|
|
128
|
-
with TabPane("Search", id="search"):
|
|
129
|
-
yield SearchPanel()
|
|
130
117
|
yield Footer()
|
|
131
118
|
logger.debug("compose() completed successfully")
|
|
132
119
|
except Exception as e:
|
|
@@ -135,8 +122,8 @@ class AlineDashboard(App):
|
|
|
135
122
|
|
|
136
123
|
def _tab_ids(self) -> list[str]:
|
|
137
124
|
if self.dev_mode:
|
|
138
|
-
return ["terminal", "watcher", "worker", "
|
|
139
|
-
return ["terminal", "
|
|
125
|
+
return ["terminal", "watcher", "worker", "config"]
|
|
126
|
+
return ["terminal", "config"]
|
|
140
127
|
|
|
141
128
|
def on_mount(self) -> None:
|
|
142
129
|
"""Apply theme based on system settings and watch for changes."""
|
|
@@ -222,14 +209,8 @@ class AlineDashboard(App):
|
|
|
222
209
|
self.query_one(WatcherPanel).refresh_data()
|
|
223
210
|
elif active_tab_id == "worker":
|
|
224
211
|
self.query_one(WorkerPanel).refresh_data()
|
|
225
|
-
elif active_tab_id == "sessions":
|
|
226
|
-
self.query_one(SessionsTable).refresh_data()
|
|
227
|
-
elif active_tab_id == "events":
|
|
228
|
-
self.query_one(EventsTable).refresh_data()
|
|
229
212
|
elif active_tab_id == "config":
|
|
230
213
|
self.query_one(ConfigPanel).refresh_data()
|
|
231
|
-
elif active_tab_id == "search":
|
|
232
|
-
pass # Search is manual
|
|
233
214
|
elif active_tab_id == "terminal":
|
|
234
215
|
await self.query_one(TerminalPanel).refresh_data()
|
|
235
216
|
|
|
@@ -242,7 +223,6 @@ class AlineDashboard(App):
|
|
|
242
223
|
self.query_one(WatcherPanel).action_next_page()
|
|
243
224
|
elif active_tab_id == "worker":
|
|
244
225
|
self.query_one(WorkerPanel).action_next_page()
|
|
245
|
-
# sessions and events tabs use scrolling instead of pagination
|
|
246
226
|
|
|
247
227
|
def action_page_prev(self) -> None:
|
|
248
228
|
"""Go to previous page in current panel."""
|
|
@@ -253,7 +233,6 @@ class AlineDashboard(App):
|
|
|
253
233
|
self.query_one(WatcherPanel).action_prev_page()
|
|
254
234
|
elif active_tab_id == "worker":
|
|
255
235
|
self.query_one(WorkerPanel).action_prev_page()
|
|
256
|
-
# sessions and events tabs use scrolling instead of pagination
|
|
257
236
|
|
|
258
237
|
def action_switch_view(self) -> None:
|
|
259
238
|
"""Switch view in current panel (if supported)."""
|
|
@@ -264,10 +243,6 @@ class AlineDashboard(App):
|
|
|
264
243
|
self.query_one(WatcherPanel).action_switch_view()
|
|
265
244
|
elif active_tab_id == "worker":
|
|
266
245
|
self.query_one(WorkerPanel).action_switch_view()
|
|
267
|
-
elif active_tab_id == "sessions":
|
|
268
|
-
self.query_one(SessionsTable).action_switch_view()
|
|
269
|
-
elif active_tab_id == "events":
|
|
270
|
-
self.query_one(EventsTable).action_switch_view()
|
|
271
246
|
|
|
272
247
|
def action_help(self) -> None:
|
|
273
248
|
"""Show help information."""
|
|
@@ -286,130 +261,6 @@ class AlineDashboard(App):
|
|
|
286
261
|
self._quit_confirm_deadline = now + self._quit_confirm_window_s
|
|
287
262
|
self.notify("Press Ctrl+C again to quit", title="Quit", timeout=2)
|
|
288
263
|
|
|
289
|
-
def action_create_event(self) -> None:
|
|
290
|
-
"""Create an event from selected sessions (Sessions tab only)."""
|
|
291
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
292
|
-
if tabbed_content.active != "sessions":
|
|
293
|
-
self.notify(
|
|
294
|
-
"Switch to the Sessions tab to create an event", title="Create Event", timeout=3
|
|
295
|
-
)
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
sessions_panel = self.query_one(SessionsTable)
|
|
299
|
-
session_ids = sessions_panel.get_selected_session_ids()
|
|
300
|
-
if not session_ids:
|
|
301
|
-
self.notify(
|
|
302
|
-
"No sessions selected (use space / cmd-click / shift-click)",
|
|
303
|
-
title="Create Event",
|
|
304
|
-
timeout=3,
|
|
305
|
-
)
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
from .screens import CreateEventScreen
|
|
309
|
-
|
|
310
|
-
self.push_screen(CreateEventScreen(session_ids))
|
|
311
|
-
|
|
312
|
-
def action_share_import(self) -> None:
|
|
313
|
-
"""Import a share URL (Events tab only)."""
|
|
314
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
315
|
-
if tabbed_content.active != "events":
|
|
316
|
-
self.notify(
|
|
317
|
-
"Switch to the Events tab to import a share", title="Share Import", timeout=3
|
|
318
|
-
)
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
from .screens import ShareImportScreen
|
|
322
|
-
|
|
323
|
-
self.push_screen(ShareImportScreen())
|
|
324
|
-
|
|
325
|
-
async def action_load_context(self) -> None:
|
|
326
|
-
"""Load selected sessions/events into the active terminal context (Claude/Codex)."""
|
|
327
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
328
|
-
active_tab_id = tabbed_content.active
|
|
329
|
-
|
|
330
|
-
try:
|
|
331
|
-
from . import tmux_manager
|
|
332
|
-
|
|
333
|
-
context_id = tmux_manager.get_active_context_id(
|
|
334
|
-
allowed_providers={"claude", "codex"}
|
|
335
|
-
)
|
|
336
|
-
except Exception:
|
|
337
|
-
context_id = None
|
|
338
|
-
|
|
339
|
-
if not context_id:
|
|
340
|
-
self.notify(
|
|
341
|
-
"No active context found. Use the Terminal tab and select a Claude ('cc') or Codex terminal.",
|
|
342
|
-
title="Load Context",
|
|
343
|
-
severity="warning",
|
|
344
|
-
timeout=4,
|
|
345
|
-
)
|
|
346
|
-
return
|
|
347
|
-
|
|
348
|
-
sessions: list[str] = []
|
|
349
|
-
events: list[str] = []
|
|
350
|
-
|
|
351
|
-
if active_tab_id == "sessions":
|
|
352
|
-
sessions = self.query_one(SessionsTable).get_selected_session_ids()
|
|
353
|
-
if not sessions:
|
|
354
|
-
self.notify(
|
|
355
|
-
"No sessions selected (use space / cmd-click / shift-click)",
|
|
356
|
-
title="Load Context",
|
|
357
|
-
severity="warning",
|
|
358
|
-
timeout=3,
|
|
359
|
-
)
|
|
360
|
-
return
|
|
361
|
-
elif active_tab_id == "events":
|
|
362
|
-
events = self.query_one(EventsTable).get_selected_event_ids()
|
|
363
|
-
if not events:
|
|
364
|
-
self.notify(
|
|
365
|
-
"No events selected (use space / cmd-click / shift-click)",
|
|
366
|
-
title="Load Context",
|
|
367
|
-
severity="warning",
|
|
368
|
-
timeout=3,
|
|
369
|
-
)
|
|
370
|
-
return
|
|
371
|
-
else:
|
|
372
|
-
self.notify(
|
|
373
|
-
"Switch to Sessions or Events to load selection into context",
|
|
374
|
-
title="Load Context",
|
|
375
|
-
timeout=3,
|
|
376
|
-
)
|
|
377
|
-
return
|
|
378
|
-
|
|
379
|
-
try:
|
|
380
|
-
from ..context import add_context
|
|
381
|
-
|
|
382
|
-
add_context(
|
|
383
|
-
sessions=sessions or None,
|
|
384
|
-
events=events or None,
|
|
385
|
-
context_id=context_id,
|
|
386
|
-
)
|
|
387
|
-
except Exception as e:
|
|
388
|
-
self.notify(
|
|
389
|
-
f"Failed to load context: {e}",
|
|
390
|
-
title="Load Context",
|
|
391
|
-
severity="error",
|
|
392
|
-
timeout=4,
|
|
393
|
-
)
|
|
394
|
-
return
|
|
395
|
-
|
|
396
|
-
try:
|
|
397
|
-
from .widgets.terminal_panel import TerminalPanel
|
|
398
|
-
|
|
399
|
-
if TerminalPanel.supported():
|
|
400
|
-
await self.query_one(TerminalPanel).refresh_data()
|
|
401
|
-
except Exception:
|
|
402
|
-
pass
|
|
403
|
-
|
|
404
|
-
parts: list[str] = []
|
|
405
|
-
if sessions:
|
|
406
|
-
parts.append(f"{len(sessions)} sessions")
|
|
407
|
-
if events:
|
|
408
|
-
parts.append(f"{len(events)} events")
|
|
409
|
-
what = ", ".join(parts) if parts else "selection"
|
|
410
|
-
self.notify(f"Loaded {what} into {context_id}", title="Load Context", timeout=3)
|
|
411
|
-
|
|
412
|
-
|
|
413
264
|
def run_dashboard(use_native_terminal: bool | None = None) -> None:
|
|
414
265
|
"""Run the Aline Dashboard.
|
|
415
266
|
|
|
@@ -195,11 +195,17 @@ def _session_id_from_transcript_path(transcript_path: str | None) -> str | None:
|
|
|
195
195
|
|
|
196
196
|
def _load_terminal_state_from_db() -> dict[str, dict[str, str]]:
|
|
197
197
|
"""Load terminal state from database (best-effort)."""
|
|
198
|
+
import time as _time
|
|
199
|
+
t0 = _time.time()
|
|
198
200
|
try:
|
|
199
201
|
from ..db import get_database
|
|
200
202
|
|
|
203
|
+
t1 = _time.time()
|
|
201
204
|
db = get_database(read_only=True)
|
|
205
|
+
logger.info(f"[PERF] _load_terminal_state_from_db get_database: {_time.time() - t1:.3f}s")
|
|
206
|
+
t2 = _time.time()
|
|
202
207
|
agents = db.list_agents(status="active", limit=100)
|
|
208
|
+
logger.info(f"[PERF] _load_terminal_state_from_db list_agents: {_time.time() - t2:.3f}s")
|
|
203
209
|
|
|
204
210
|
out: dict[str, dict[str, str]] = {}
|
|
205
211
|
for agent in agents:
|
|
@@ -510,8 +516,18 @@ def bootstrap_dashboard_into_tmux() -> None:
|
|
|
510
516
|
os.execvp("tmux", ["tmux", "-L", OUTER_SOCKET, "attach", "-t", OUTER_SESSION])
|
|
511
517
|
|
|
512
518
|
|
|
519
|
+
_inner_session_configured = False
|
|
520
|
+
|
|
521
|
+
|
|
513
522
|
def ensure_inner_session() -> bool:
|
|
514
|
-
"""Ensure the inner tmux server/session exists (returns True on success).
|
|
523
|
+
"""Ensure the inner tmux server/session exists (returns True on success).
|
|
524
|
+
|
|
525
|
+
The full configuration (mouse, status bar, border styles, home window setup) is
|
|
526
|
+
only applied once per process lifetime. Subsequent calls just verify the session
|
|
527
|
+
is still alive via a cheap ``has-session`` check.
|
|
528
|
+
"""
|
|
529
|
+
global _inner_session_configured
|
|
530
|
+
|
|
515
531
|
if not (tmux_available() and in_tmux() and managed_env_enabled()):
|
|
516
532
|
return False
|
|
517
533
|
|
|
@@ -523,6 +539,13 @@ def ensure_inner_session() -> bool:
|
|
|
523
539
|
!= 0
|
|
524
540
|
):
|
|
525
541
|
return False
|
|
542
|
+
# Force re-configuration after creating a new session.
|
|
543
|
+
_inner_session_configured = False
|
|
544
|
+
|
|
545
|
+
if _inner_session_configured:
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
# --- One-time configuration below ---
|
|
526
549
|
|
|
527
550
|
# Ensure the default/home window stays named "home" (tmux auto-rename would otherwise
|
|
528
551
|
# change it to "zsh"/"opencode" depending on the last foreground command).
|
|
@@ -546,6 +569,8 @@ def ensure_inner_session() -> bool:
|
|
|
546
569
|
_run_inner_tmux(["set-option", "-g", "pane-border-indicators", "arrows"])
|
|
547
570
|
|
|
548
571
|
_source_aline_tmux_config(_run_inner_tmux)
|
|
572
|
+
|
|
573
|
+
_inner_session_configured = True
|
|
549
574
|
return True
|
|
550
575
|
|
|
551
576
|
|
|
@@ -632,14 +657,12 @@ def _ensure_inner_home_window() -> None:
|
|
|
632
657
|
_run_inner_tmux(["set-option", "-w", "-t", window_id, "allow-rename", "off"])
|
|
633
658
|
|
|
634
659
|
# Mark as internal/no-track so UI can hide it.
|
|
660
|
+
# NOTE: We use _run_inner_tmux directly here instead of set_inner_window_options
|
|
661
|
+
# to avoid recursion: set_inner_window_options → ensure_inner_session →
|
|
662
|
+
# _ensure_inner_home_window → set_inner_window_options.
|
|
635
663
|
try:
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
{
|
|
639
|
-
OPT_NO_TRACK: "1",
|
|
640
|
-
OPT_CREATED_AT: str(time.time()),
|
|
641
|
-
},
|
|
642
|
-
)
|
|
664
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, OPT_NO_TRACK, "1"])
|
|
665
|
+
_run_inner_tmux(["set-option", "-w", "-t", window_id, OPT_CREATED_AT, str(time.time())])
|
|
643
666
|
except Exception:
|
|
644
667
|
pass
|
|
645
668
|
|
|
@@ -687,9 +710,14 @@ def ensure_right_pane(width_percent: int = 50) -> bool:
|
|
|
687
710
|
|
|
688
711
|
|
|
689
712
|
def list_inner_windows() -> list[InnerWindow]:
|
|
713
|
+
import time as _time
|
|
714
|
+
t0 = _time.time()
|
|
690
715
|
if not ensure_inner_session():
|
|
691
716
|
return []
|
|
717
|
+
logger.info(f"[PERF] list_inner_windows ensure_inner_session: {_time.time() - t0:.3f}s")
|
|
718
|
+
t1 = _time.time()
|
|
692
719
|
state = _load_terminal_state()
|
|
720
|
+
logger.info(f"[PERF] list_inner_windows _load_terminal_state: {_time.time() - t1:.3f}s")
|
|
693
721
|
out = (
|
|
694
722
|
_run_inner_tmux(
|
|
695
723
|
[
|
|
@@ -785,13 +813,16 @@ def list_inner_windows() -> list[InnerWindow]:
|
|
|
785
813
|
|
|
786
814
|
|
|
787
815
|
def set_inner_window_options(window_id: str, options: dict[str, str]) -> bool:
|
|
816
|
+
import time as _time
|
|
788
817
|
if not ensure_inner_session():
|
|
789
818
|
return False
|
|
790
819
|
ok = True
|
|
791
820
|
for key, value in options.items():
|
|
821
|
+
t0 = _time.time()
|
|
792
822
|
# Important: these are per-window (not session-wide) to avoid cross-tab clobbering.
|
|
793
823
|
if _run_inner_tmux(["set-option", "-w", "-t", window_id, key, value]).returncode != 0:
|
|
794
824
|
ok = False
|
|
825
|
+
logger.info(f"[PERF] set_inner_window_options {key}: {_time.time() - t0:.3f}s")
|
|
795
826
|
return ok
|
|
796
827
|
|
|
797
828
|
|
|
@@ -810,15 +841,22 @@ def create_inner_window(
|
|
|
810
841
|
context_id: str | None = None,
|
|
811
842
|
no_track: bool = False,
|
|
812
843
|
) -> InnerWindow | None:
|
|
844
|
+
import time as _time
|
|
845
|
+
t0 = _time.time()
|
|
846
|
+
logger.info(f"[PERF] create_inner_window START")
|
|
813
847
|
if not ensure_right_pane():
|
|
814
848
|
return None
|
|
849
|
+
logger.info(f"[PERF] create_inner_window ensure_right_pane: {_time.time() - t0:.3f}s")
|
|
815
850
|
|
|
851
|
+
t1 = _time.time()
|
|
816
852
|
existing = list_inner_windows()
|
|
853
|
+
logger.info(f"[PERF] create_inner_window list_inner_windows: {_time.time() - t1:.3f}s")
|
|
817
854
|
name = _unique_name((w.window_name for w in existing), base_name)
|
|
818
855
|
|
|
819
856
|
# Record creation time before creating the window
|
|
820
857
|
created_at = time.time()
|
|
821
858
|
|
|
859
|
+
t2 = _time.time()
|
|
822
860
|
proc = _run_inner_tmux(
|
|
823
861
|
[
|
|
824
862
|
"new-window",
|
|
@@ -833,6 +871,7 @@ def create_inner_window(
|
|
|
833
871
|
],
|
|
834
872
|
capture=True,
|
|
835
873
|
)
|
|
874
|
+
logger.info(f"[PERF] create_inner_window new-window: {_time.time() - t2:.3f}s")
|
|
836
875
|
if proc.returncode != 0:
|
|
837
876
|
return None
|
|
838
877
|
|
|
@@ -856,7 +895,9 @@ def create_inner_window(
|
|
|
856
895
|
opts[OPT_NO_TRACK] = "1"
|
|
857
896
|
else:
|
|
858
897
|
opts.setdefault(OPT_NO_TRACK, "")
|
|
898
|
+
t3 = _time.time()
|
|
859
899
|
set_inner_window_options(window_id, opts)
|
|
900
|
+
logger.info(f"[PERF] create_inner_window set_options: {_time.time() - t3:.3f}s")
|
|
860
901
|
|
|
861
902
|
_run_inner_tmux(["select-window", "-t", window_id])
|
|
862
903
|
|
|
@@ -393,26 +393,32 @@ class ConfigPanel(Static):
|
|
|
393
393
|
self.app.notify(f"Error saving setting: {e}", title="Config", severity="error")
|
|
394
394
|
|
|
395
395
|
def _handle_doctor(self) -> None:
|
|
396
|
-
"""Run aline doctor
|
|
396
|
+
"""Run aline doctor directly in background thread."""
|
|
397
397
|
self.app.notify("Running Aline Doctor...", title="Doctor")
|
|
398
398
|
|
|
399
399
|
def do_doctor():
|
|
400
400
|
try:
|
|
401
|
-
import
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
401
|
+
import contextlib
|
|
402
|
+
import io
|
|
403
|
+
from ...commands.doctor import run_doctor
|
|
404
|
+
|
|
405
|
+
# Suppress Rich console output (would corrupt TUI)
|
|
406
|
+
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
407
|
+
exit_code = run_doctor(
|
|
408
|
+
restart_daemons=True,
|
|
409
|
+
start_if_not_running=False,
|
|
410
|
+
verbose=False,
|
|
411
|
+
clear_cache=True,
|
|
412
|
+
auto_fix=True,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if exit_code == 0:
|
|
409
416
|
self.app.call_from_thread(
|
|
410
417
|
self.app.notify, "Doctor completed successfully", title="Doctor"
|
|
411
418
|
)
|
|
412
419
|
else:
|
|
413
|
-
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
|
414
420
|
self.app.call_from_thread(
|
|
415
|
-
self.app.notify,
|
|
421
|
+
self.app.notify, "Doctor completed with errors", title="Doctor", severity="error"
|
|
416
422
|
)
|
|
417
423
|
except Exception as e:
|
|
418
424
|
self.app.call_from_thread(
|
|
@@ -668,13 +668,17 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
668
668
|
|
|
669
669
|
async def refresh_data(self) -> None:
|
|
670
670
|
async with self._refresh_lock:
|
|
671
|
+
t_start = time.time()
|
|
671
672
|
# Check and close stale terminals if enabled
|
|
672
673
|
await self._close_stale_terminals_if_enabled()
|
|
674
|
+
logger.debug(f"[PERF] _close_stale_terminals_if_enabled: {time.time() - t_start:.3f}s")
|
|
673
675
|
|
|
676
|
+
t_refresh = time.time()
|
|
674
677
|
if self._is_native_mode():
|
|
675
678
|
await self._refresh_native_data()
|
|
676
679
|
else:
|
|
677
680
|
await self._refresh_tmux_data()
|
|
681
|
+
logger.debug(f"[PERF] total refresh: {time.time() - t_start:.3f}s")
|
|
678
682
|
|
|
679
683
|
async def _close_stale_terminals_if_enabled(self) -> None:
|
|
680
684
|
"""Close terminals that haven't been updated for the configured hours."""
|
|
@@ -741,15 +745,13 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
741
745
|
logger.error(f"Failed to list native terminals: {e}")
|
|
742
746
|
return
|
|
743
747
|
|
|
744
|
-
#
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
except Exception:
|
|
752
|
-
pass
|
|
748
|
+
# Yield to event loop to keep UI responsive
|
|
749
|
+
await asyncio.sleep(0)
|
|
750
|
+
|
|
751
|
+
# NOTE: _maybe_link_codex_session_for_terminal is intentionally skipped here
|
|
752
|
+
# because it performs expensive file system scans (find_codex_sessions_for_project)
|
|
753
|
+
# that can take minutes with many session files. Codex session linking is handled
|
|
754
|
+
# by the watcher process instead.
|
|
753
755
|
|
|
754
756
|
active_window_id = next(
|
|
755
757
|
(w.session_id for w in windows if w.active), None
|
|
@@ -765,6 +767,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
765
767
|
]
|
|
766
768
|
titles = self._fetch_claude_session_titles(claude_ids)
|
|
767
769
|
|
|
770
|
+
# Yield to event loop after DB query
|
|
771
|
+
await asyncio.sleep(0)
|
|
772
|
+
|
|
768
773
|
# Get context info
|
|
769
774
|
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
770
775
|
all_context_session_ids: set[str] = set()
|
|
@@ -793,6 +798,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
793
798
|
|
|
794
799
|
async def _refresh_tmux_data(self) -> None:
|
|
795
800
|
"""Refresh data using tmux backend."""
|
|
801
|
+
t0 = time.time()
|
|
796
802
|
try:
|
|
797
803
|
supported = self.supported()
|
|
798
804
|
except Exception:
|
|
@@ -806,24 +812,29 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
806
812
|
except Exception:
|
|
807
813
|
return
|
|
808
814
|
windows = [w for w in windows if not self._is_internal_tmux_window(w)]
|
|
815
|
+
logger.debug(f"[PERF] list_inner_windows: {time.time() - t0:.3f}s")
|
|
809
816
|
|
|
810
|
-
#
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
except Exception:
|
|
818
|
-
pass
|
|
817
|
+
# Yield to event loop to keep UI responsive
|
|
818
|
+
await asyncio.sleep(0)
|
|
819
|
+
|
|
820
|
+
# NOTE: _maybe_link_codex_session_for_terminal is intentionally skipped here
|
|
821
|
+
# because it performs expensive file system scans (find_codex_sessions_for_project)
|
|
822
|
+
# that can take minutes with many session files. Codex session linking is handled
|
|
823
|
+
# by the watcher process instead.
|
|
819
824
|
|
|
820
825
|
active_window_id = next((w.window_id for w in windows if w.active), None)
|
|
821
826
|
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
822
827
|
self._expanded_window_id = None
|
|
823
828
|
|
|
829
|
+
t1 = time.time()
|
|
824
830
|
session_ids = [w.session_id for w in windows if self._supports_context(w) and w.session_id]
|
|
825
831
|
titles = self._fetch_claude_session_titles(session_ids)
|
|
832
|
+
logger.debug(f"[PERF] fetch_claude_session_titles: {time.time() - t1:.3f}s")
|
|
833
|
+
|
|
834
|
+
# Yield to event loop after DB query
|
|
835
|
+
await asyncio.sleep(0)
|
|
826
836
|
|
|
837
|
+
t2 = time.time()
|
|
827
838
|
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
828
839
|
all_context_session_ids: set[str] = set()
|
|
829
840
|
for w in windows:
|
|
@@ -840,14 +851,21 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
840
851
|
event_count,
|
|
841
852
|
)
|
|
842
853
|
all_context_session_ids.update(session_ids)
|
|
854
|
+
# Yield periodically during context info gathering
|
|
855
|
+
await asyncio.sleep(0)
|
|
856
|
+
logger.debug(f"[PERF] get_loaded_context_info loop: {time.time() - t2:.3f}s")
|
|
843
857
|
|
|
858
|
+
t3 = time.time()
|
|
844
859
|
if all_context_session_ids:
|
|
845
860
|
titles.update(self._fetch_claude_session_titles(sorted(all_context_session_ids)))
|
|
861
|
+
logger.debug(f"[PERF] fetch context session titles: {time.time() - t3:.3f}s")
|
|
846
862
|
|
|
863
|
+
t4 = time.time()
|
|
847
864
|
try:
|
|
848
865
|
await self._render_terminals_tmux(windows, titles, context_info_by_context_id)
|
|
849
866
|
except Exception:
|
|
850
867
|
return
|
|
868
|
+
logger.debug(f"[PERF] render_terminals_tmux: {time.time() - t4:.3f}s")
|
|
851
869
|
|
|
852
870
|
def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
|
|
853
871
|
# Back-compat hook for tests and older call sites.
|
|
@@ -1232,13 +1250,23 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1232
1250
|
return
|
|
1233
1251
|
|
|
1234
1252
|
agent_type, workspace, skip_permissions, no_track = result
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1253
|
+
|
|
1254
|
+
# Capture self reference for use in the deferred callback
|
|
1255
|
+
panel = self
|
|
1256
|
+
|
|
1257
|
+
# Use app.call_later to defer worker creation until after the modal is dismissed.
|
|
1258
|
+
# This ensures the modal screen is fully closed before the worker starts,
|
|
1259
|
+
# preventing UI update conflicts between modal closing and terminal panel refresh.
|
|
1260
|
+
def start_worker() -> None:
|
|
1261
|
+
panel.run_worker(
|
|
1262
|
+
panel._create_agent(
|
|
1263
|
+
agent_type, workspace, skip_permissions=skip_permissions, no_track=no_track
|
|
1264
|
+
),
|
|
1265
|
+
group="terminal-panel-create",
|
|
1266
|
+
exclusive=True,
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
self.app.call_later(start_worker)
|
|
1242
1270
|
|
|
1243
1271
|
async def _create_agent(
|
|
1244
1272
|
self, agent_type: str, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
@@ -1252,7 +1280,14 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1252
1280
|
await self._create_opencode_terminal(workspace)
|
|
1253
1281
|
elif agent_type == "zsh":
|
|
1254
1282
|
await self._create_zsh_terminal(workspace)
|
|
1255
|
-
|
|
1283
|
+
# Schedule refresh in a separate worker to avoid blocking UI.
|
|
1284
|
+
# The refresh involves slow synchronous operations (DB queries, file scans)
|
|
1285
|
+
# that would otherwise freeze the dashboard.
|
|
1286
|
+
self.run_worker(
|
|
1287
|
+
self.refresh_data(),
|
|
1288
|
+
group="terminal-panel-refresh",
|
|
1289
|
+
exclusive=True,
|
|
1290
|
+
)
|
|
1256
1291
|
|
|
1257
1292
|
async def _create_claude_terminal(
|
|
1258
1293
|
self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
|
|
@@ -1495,6 +1530,8 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1495
1530
|
|
|
1496
1531
|
async def _create_zsh_terminal(self, workspace: str) -> None:
|
|
1497
1532
|
"""Create a new zsh terminal."""
|
|
1533
|
+
t0 = time.time()
|
|
1534
|
+
logger.info(f"[PERF] _create_zsh_terminal START")
|
|
1498
1535
|
if self._is_native_mode():
|
|
1499
1536
|
backend = await self._ensure_native_backend()
|
|
1500
1537
|
if backend:
|
|
@@ -1509,13 +1546,19 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1509
1546
|
self.app.notify(
|
|
1510
1547
|
"Failed to open zsh terminal", title="Terminal", severity="error"
|
|
1511
1548
|
)
|
|
1549
|
+
logger.info(f"[PERF] _create_zsh_terminal native END: {time.time() - t0:.3f}s")
|
|
1512
1550
|
return
|
|
1513
1551
|
|
|
1514
1552
|
# Tmux fallback
|
|
1553
|
+
t1 = time.time()
|
|
1515
1554
|
command = self._command_in_directory("zsh", workspace)
|
|
1555
|
+
logger.info(f"[PERF] _create_zsh_terminal command ready: {time.time() - t1:.3f}s")
|
|
1556
|
+
t2 = time.time()
|
|
1516
1557
|
created = tmux_manager.create_inner_window("zsh", command)
|
|
1558
|
+
logger.info(f"[PERF] _create_zsh_terminal create_inner_window: {time.time() - t2:.3f}s")
|
|
1517
1559
|
if not created:
|
|
1518
1560
|
self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
|
|
1561
|
+
logger.info(f"[PERF] _create_zsh_terminal TOTAL: {time.time() - t0:.3f}s")
|
|
1519
1562
|
|
|
1520
1563
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
1521
1564
|
button_id = event.button.id or ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|