aline-ai 0.5.4__py3-none-any.whl → 0.5.6__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.5.4.dist-info → aline_ai-0.5.6.dist-info}/METADATA +1 -1
- aline_ai-0.5.6.dist-info/RECORD +95 -0
- realign/__init__.py +1 -1
- realign/adapters/antigravity.py +28 -20
- realign/adapters/base.py +46 -50
- realign/adapters/claude.py +14 -14
- realign/adapters/codex.py +7 -7
- realign/adapters/gemini.py +11 -11
- realign/adapters/registry.py +14 -10
- realign/claude_detector.py +2 -2
- realign/claude_hooks/__init__.py +3 -3
- realign/claude_hooks/permission_request_hook_installer.py +31 -32
- realign/claude_hooks/stop_hook.py +4 -1
- realign/claude_hooks/stop_hook_installer.py +30 -31
- realign/cli.py +23 -4
- realign/codex_detector.py +11 -11
- realign/commands/add.py +88 -65
- realign/commands/config.py +3 -12
- realign/commands/context.py +3 -1
- realign/commands/export_shares.py +86 -127
- realign/commands/import_shares.py +145 -155
- realign/commands/init.py +166 -30
- realign/commands/restore.py +18 -6
- realign/commands/search.py +14 -42
- realign/commands/upgrade.py +155 -11
- realign/commands/watcher.py +98 -219
- realign/commands/worker.py +29 -6
- realign/config.py +25 -20
- realign/context.py +1 -3
- realign/dashboard/app.py +34 -24
- realign/dashboard/screens/__init__.py +10 -1
- realign/dashboard/screens/create_agent.py +244 -0
- realign/dashboard/screens/create_event.py +3 -1
- realign/dashboard/screens/event_detail.py +14 -6
- realign/dashboard/screens/help_screen.py +114 -0
- realign/dashboard/screens/session_detail.py +3 -1
- realign/dashboard/screens/share_import.py +7 -3
- realign/dashboard/tmux_manager.py +54 -9
- realign/dashboard/widgets/config_panel.py +85 -1
- realign/dashboard/widgets/events_table.py +314 -70
- realign/dashboard/widgets/header.py +2 -1
- realign/dashboard/widgets/search_panel.py +37 -27
- realign/dashboard/widgets/sessions_table.py +404 -85
- realign/dashboard/widgets/terminal_panel.py +155 -175
- realign/dashboard/widgets/watcher_panel.py +6 -2
- realign/dashboard/widgets/worker_panel.py +10 -1
- realign/db/__init__.py +1 -1
- realign/db/base.py +5 -15
- realign/db/locks.py +0 -1
- realign/db/migration.py +82 -76
- realign/db/schema.py +2 -6
- realign/db/sqlite_db.py +23 -41
- realign/events/__init__.py +0 -1
- realign/events/event_summarizer.py +27 -15
- realign/events/session_summarizer.py +29 -15
- realign/file_lock.py +1 -0
- realign/hooks.py +150 -60
- realign/logging_config.py +12 -15
- realign/mcp_server.py +30 -51
- realign/mcp_watcher.py +0 -1
- realign/models/event.py +29 -20
- realign/prompts/__init__.py +7 -7
- realign/prompts/presets.py +15 -11
- realign/redactor.py +99 -59
- realign/triggers/__init__.py +9 -9
- realign/triggers/antigravity_trigger.py +30 -28
- realign/triggers/base.py +4 -3
- realign/triggers/claude_trigger.py +104 -85
- realign/triggers/codex_trigger.py +15 -5
- realign/triggers/gemini_trigger.py +57 -47
- realign/triggers/next_turn_trigger.py +3 -1
- realign/triggers/registry.py +6 -2
- realign/triggers/turn_status.py +3 -1
- realign/watcher_core.py +306 -131
- realign/watcher_daemon.py +8 -8
- realign/worker_core.py +3 -1
- realign/worker_daemon.py +3 -1
- aline_ai-0.5.4.dist-info/RECORD +0 -93
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,6 @@ import asyncio
|
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
13
|
import shlex
|
|
14
|
-
import subprocess
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
from typing import Callable
|
|
17
16
|
|
|
@@ -61,8 +60,7 @@ class _SignalFileWatcher:
|
|
|
61
60
|
try:
|
|
62
61
|
if PERMISSION_SIGNAL_DIR.exists():
|
|
63
62
|
self._seen_files = {
|
|
64
|
-
f.name for f in PERMISSION_SIGNAL_DIR.iterdir()
|
|
65
|
-
if f.suffix == ".signal"
|
|
63
|
+
f.name for f in PERMISSION_SIGNAL_DIR.iterdir() if f.suffix == ".signal"
|
|
66
64
|
}
|
|
67
65
|
except Exception:
|
|
68
66
|
self._seen_files = set()
|
|
@@ -89,8 +87,7 @@ class _SignalFileWatcher:
|
|
|
89
87
|
|
|
90
88
|
# Check for new signal files
|
|
91
89
|
current_files = {
|
|
92
|
-
f.name for f in PERMISSION_SIGNAL_DIR.iterdir()
|
|
93
|
-
if f.suffix == ".signal"
|
|
90
|
+
f.name for f in PERMISSION_SIGNAL_DIR.iterdir() if f.suffix == ".signal"
|
|
94
91
|
}
|
|
95
92
|
new_files = current_files - self._seen_files
|
|
96
93
|
|
|
@@ -115,7 +112,7 @@ class _SignalFileWatcher:
|
|
|
115
112
|
files = sorted(
|
|
116
113
|
PERMISSION_SIGNAL_DIR.glob("*.signal"),
|
|
117
114
|
key=lambda f: f.stat().st_mtime,
|
|
118
|
-
reverse=True
|
|
115
|
+
reverse=True,
|
|
119
116
|
)
|
|
120
117
|
# Keep only the 10 most recent
|
|
121
118
|
for f in files[10:]:
|
|
@@ -132,6 +129,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
132
129
|
|
|
133
130
|
class PermissionRequestDetected(Message):
|
|
134
131
|
"""Posted when a new permission request signal file is detected."""
|
|
132
|
+
|
|
135
133
|
pass
|
|
136
134
|
|
|
137
135
|
DEFAULT_CSS = """
|
|
@@ -295,14 +293,17 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
295
293
|
def compose(self) -> ComposeResult:
|
|
296
294
|
controls_enabled = self.supported()
|
|
297
295
|
with Horizontal(classes="summary"):
|
|
298
|
-
yield Button(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
yield Button(
|
|
297
|
+
"+ Create",
|
|
298
|
+
id="new-agent",
|
|
299
|
+
variant="primary",
|
|
300
|
+
disabled=not controls_enabled,
|
|
301
|
+
)
|
|
303
302
|
with Vertical(id="terminals", classes="list"):
|
|
304
303
|
if controls_enabled:
|
|
305
|
-
yield Static(
|
|
304
|
+
yield Static(
|
|
305
|
+
"No terminals yet. Click 'Create' to open a new agent terminal."
|
|
306
|
+
)
|
|
306
307
|
else:
|
|
307
308
|
yield Static(self._support_message())
|
|
308
309
|
|
|
@@ -356,21 +357,15 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
356
357
|
async with self._refresh_lock:
|
|
357
358
|
try:
|
|
358
359
|
supported = self.supported()
|
|
359
|
-
except Exception
|
|
360
|
-
self._set_status(f"Terminal support check failed: {e}")
|
|
360
|
+
except Exception:
|
|
361
361
|
return
|
|
362
362
|
|
|
363
363
|
if not supported:
|
|
364
|
-
try:
|
|
365
|
-
self._set_status(self._support_message())
|
|
366
|
-
except Exception:
|
|
367
|
-
self._set_status("Terminal not supported")
|
|
368
364
|
return
|
|
369
365
|
|
|
370
366
|
try:
|
|
371
367
|
windows = tmux_manager.list_inner_windows()
|
|
372
|
-
except Exception
|
|
373
|
-
self._set_status(f"Failed to query tmux windows: {e}")
|
|
368
|
+
except Exception:
|
|
374
369
|
return
|
|
375
370
|
active_window_id = next((w.window_id for w in windows if w.active), None)
|
|
376
371
|
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
@@ -385,10 +380,16 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
385
380
|
for w in windows:
|
|
386
381
|
if not self._is_claude_window(w) or not w.context_id:
|
|
387
382
|
continue
|
|
388
|
-
session_ids, session_count, event_count = self._get_loaded_context_info(
|
|
383
|
+
session_ids, session_count, event_count = self._get_loaded_context_info(
|
|
384
|
+
w.context_id
|
|
385
|
+
)
|
|
389
386
|
if not session_ids and session_count == 0 and event_count == 0:
|
|
390
387
|
continue
|
|
391
|
-
context_info_by_context_id[w.context_id] = (
|
|
388
|
+
context_info_by_context_id[w.context_id] = (
|
|
389
|
+
session_ids,
|
|
390
|
+
session_count,
|
|
391
|
+
event_count,
|
|
392
|
+
)
|
|
392
393
|
all_context_session_ids.update(session_ids)
|
|
393
394
|
|
|
394
395
|
if all_context_session_ids:
|
|
@@ -396,10 +397,8 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
396
397
|
|
|
397
398
|
try:
|
|
398
399
|
await self._render_terminals(windows, titles, context_info_by_context_id)
|
|
399
|
-
except Exception
|
|
400
|
-
self._set_status(f"Failed to render terminal list: {e}")
|
|
400
|
+
except Exception:
|
|
401
401
|
return
|
|
402
|
-
self._set_status(f"{len(windows)} terminals")
|
|
403
402
|
|
|
404
403
|
def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
|
|
405
404
|
if not session_ids:
|
|
@@ -464,12 +463,6 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
464
463
|
except Exception:
|
|
465
464
|
return ([], 0, 0)
|
|
466
465
|
|
|
467
|
-
def _set_status(self, text: str) -> None:
|
|
468
|
-
try:
|
|
469
|
-
self.query_one("#status", Static).update(text)
|
|
470
|
-
except Exception:
|
|
471
|
-
pass
|
|
472
|
-
|
|
473
466
|
async def _render_terminals(
|
|
474
467
|
self,
|
|
475
468
|
windows: list[tmux_manager.InnerWindow],
|
|
@@ -481,7 +474,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
481
474
|
|
|
482
475
|
if not windows:
|
|
483
476
|
await container.mount(
|
|
484
|
-
Static("No terminals yet. Click '
|
|
477
|
+
Static("No terminals yet. Click 'Create' to open a new agent terminal.")
|
|
485
478
|
)
|
|
486
479
|
return
|
|
487
480
|
|
|
@@ -516,9 +509,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
516
509
|
if w.active and can_toggle_ctx:
|
|
517
510
|
await row.mount(
|
|
518
511
|
Button(
|
|
519
|
-
|
|
520
|
-
# initial layout / tab switching).
|
|
521
|
-
"⮟" if expanded else "⮞",
|
|
512
|
+
"▼" if expanded else "▶",
|
|
522
513
|
id=f"toggle-{safe}",
|
|
523
514
|
name=w.window_id,
|
|
524
515
|
variant="default",
|
|
@@ -536,12 +527,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
536
527
|
)
|
|
537
528
|
)
|
|
538
529
|
|
|
539
|
-
if (
|
|
540
|
-
w.active
|
|
541
|
-
and self._is_claude_window(w)
|
|
542
|
-
and w.context_id
|
|
543
|
-
and expanded
|
|
544
|
-
):
|
|
530
|
+
if w.active and self._is_claude_window(w) and w.context_id and expanded:
|
|
545
531
|
ctx = VerticalScroll(id=f"ctx-{safe}", classes="context-sessions")
|
|
546
532
|
await container.mount(ctx)
|
|
547
533
|
if loaded_ids:
|
|
@@ -591,16 +577,17 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
591
577
|
details.append(header)
|
|
592
578
|
details.append("\n")
|
|
593
579
|
|
|
594
|
-
#
|
|
595
|
-
|
|
596
|
-
detail_line = f"[{window_name}]" if window_name else "claude"
|
|
580
|
+
# Build detail line with Claude label
|
|
581
|
+
detail_line = "[Claude]"
|
|
597
582
|
if w.session_id:
|
|
598
583
|
detail_line = f"{detail_line} #{self._short_id(w.session_id)}"
|
|
599
584
|
if w.active:
|
|
600
585
|
loaded_count = raw_sessions + raw_events
|
|
601
586
|
detail_line = f"{detail_line} | loaded context: {loaded_count}"
|
|
602
587
|
else:
|
|
603
|
-
detail_line =
|
|
588
|
+
detail_line = (
|
|
589
|
+
f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
|
|
590
|
+
)
|
|
604
591
|
details.append(detail_line, style="dim not bold")
|
|
605
592
|
return details
|
|
606
593
|
|
|
@@ -621,156 +608,149 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
621
608
|
return f"w-{safe}"
|
|
622
609
|
return safe
|
|
623
610
|
|
|
624
|
-
async def _select_workspace(self, prompt: str = "Select workspace") -> str | None:
|
|
625
|
-
"""Open macOS folder picker and return selected path, or None if cancelled."""
|
|
626
|
-
try:
|
|
627
|
-
default_path = os.getcwd()
|
|
628
|
-
except Exception:
|
|
629
|
-
# Can happen if the original working directory was deleted/moved.
|
|
630
|
-
default_path = str(Path.home())
|
|
631
|
-
# Use osascript to invoke macOS native folder picker
|
|
632
|
-
default_path_escaped = default_path.replace('"', '\\"')
|
|
633
|
-
prompt_escaped = prompt.replace('"', '\\"')
|
|
634
|
-
script = f'''
|
|
635
|
-
set defaultFolder to POSIX file "{default_path_escaped}" as alias
|
|
636
|
-
try
|
|
637
|
-
set selectedFolder to choose folder with prompt "{prompt_escaped}" default location defaultFolder
|
|
638
|
-
return POSIX path of selectedFolder
|
|
639
|
-
on error
|
|
640
|
-
return ""
|
|
641
|
-
end try
|
|
642
|
-
'''
|
|
643
|
-
try:
|
|
644
|
-
proc = await asyncio.get_event_loop().run_in_executor(
|
|
645
|
-
None,
|
|
646
|
-
lambda: subprocess.run(
|
|
647
|
-
["osascript", "-e", script],
|
|
648
|
-
capture_output=True,
|
|
649
|
-
text=True,
|
|
650
|
-
timeout=120,
|
|
651
|
-
),
|
|
652
|
-
)
|
|
653
|
-
result = (proc.stdout or "").strip()
|
|
654
|
-
if result and os.path.isdir(result):
|
|
655
|
-
return result
|
|
656
|
-
return None
|
|
657
|
-
except Exception:
|
|
658
|
-
return None
|
|
659
|
-
|
|
660
611
|
@staticmethod
|
|
661
612
|
def _command_in_directory(command: str, directory: str) -> str:
|
|
662
613
|
"""Wrap a command to run in a specific directory."""
|
|
663
614
|
return f"cd {shlex.quote(directory)} && {command}"
|
|
664
615
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if button_id == "refresh":
|
|
669
|
-
await self.refresh_data()
|
|
616
|
+
def _on_create_agent_result(self, result: tuple[str, str] | None) -> None:
|
|
617
|
+
"""Handle the result from CreateAgentScreen modal."""
|
|
618
|
+
if result is None:
|
|
670
619
|
return
|
|
671
620
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return
|
|
679
|
-
|
|
680
|
-
if button_id == "new-cc":
|
|
681
|
-
workspace = await self._select_workspace("Select workspace for Claude")
|
|
682
|
-
if not workspace:
|
|
683
|
-
return
|
|
621
|
+
agent_type, workspace = result
|
|
622
|
+
self.run_worker(
|
|
623
|
+
self._create_agent(agent_type, workspace),
|
|
624
|
+
group="terminal-panel-create",
|
|
625
|
+
exclusive=True,
|
|
626
|
+
)
|
|
684
627
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
628
|
+
async def _create_agent(self, agent_type: str, workspace: str) -> None:
|
|
629
|
+
"""Create a new agent terminal based on the selected type and workspace."""
|
|
630
|
+
if agent_type == "claude":
|
|
631
|
+
await self._create_claude_terminal(workspace)
|
|
632
|
+
elif agent_type == "codex":
|
|
633
|
+
await self._create_codex_terminal(workspace)
|
|
634
|
+
elif agent_type == "opencode":
|
|
635
|
+
await self._create_opencode_terminal(workspace)
|
|
636
|
+
elif agent_type == "zsh":
|
|
637
|
+
await self._create_zsh_terminal(workspace)
|
|
638
|
+
await self.refresh_data()
|
|
639
|
+
|
|
640
|
+
async def _create_claude_terminal(self, workspace: str) -> None:
|
|
641
|
+
"""Create a new Claude terminal."""
|
|
642
|
+
terminal_id = tmux_manager.new_terminal_id()
|
|
643
|
+
context_id = tmux_manager.new_context_id("cc")
|
|
644
|
+
env = {
|
|
645
|
+
tmux_manager.ENV_TERMINAL_ID: terminal_id,
|
|
646
|
+
tmux_manager.ENV_TERMINAL_PROVIDER: "claude",
|
|
647
|
+
tmux_manager.ENV_INNER_SOCKET: tmux_manager.INNER_SOCKET,
|
|
648
|
+
tmux_manager.ENV_INNER_SESSION: tmux_manager.INNER_SESSION,
|
|
649
|
+
tmux_manager.ENV_CONTEXT_ID: context_id,
|
|
650
|
+
}
|
|
694
651
|
|
|
695
|
-
|
|
696
|
-
|
|
652
|
+
try:
|
|
653
|
+
from ...claude_hooks.stop_hook_installer import (
|
|
654
|
+
ensure_stop_hook_installed,
|
|
655
|
+
get_settings_path as get_stop_settings_path,
|
|
656
|
+
install_stop_hook,
|
|
657
|
+
)
|
|
658
|
+
from ...claude_hooks.user_prompt_submit_hook_installer import (
|
|
659
|
+
ensure_user_prompt_submit_hook_installed,
|
|
660
|
+
get_settings_path as get_submit_settings_path,
|
|
661
|
+
install_user_prompt_submit_hook,
|
|
662
|
+
)
|
|
663
|
+
from ...claude_hooks.permission_request_hook_installer import (
|
|
664
|
+
ensure_permission_request_hook_installed,
|
|
665
|
+
get_settings_path as get_permission_settings_path,
|
|
666
|
+
install_permission_request_hook,
|
|
667
|
+
)
|
|
697
668
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
install_stop_hook,
|
|
702
|
-
)
|
|
703
|
-
from ...claude_hooks.user_prompt_submit_hook_installer import (
|
|
704
|
-
ensure_user_prompt_submit_hook_installed,
|
|
705
|
-
get_settings_path as get_submit_settings_path,
|
|
706
|
-
install_user_prompt_submit_hook,
|
|
707
|
-
)
|
|
708
|
-
from ...claude_hooks.permission_request_hook_installer import (
|
|
709
|
-
ensure_permission_request_hook_installed,
|
|
710
|
-
get_settings_path as get_permission_settings_path,
|
|
711
|
-
install_permission_request_hook,
|
|
712
|
-
)
|
|
669
|
+
ok_global_stop = ensure_stop_hook_installed(quiet=True)
|
|
670
|
+
ok_global_submit = ensure_user_prompt_submit_hook_installed(quiet=True)
|
|
671
|
+
ok_global_permission = ensure_permission_request_hook_installed(quiet=True)
|
|
713
672
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
673
|
+
project_root = Path(workspace)
|
|
674
|
+
ok_project_stop = install_stop_hook(
|
|
675
|
+
get_stop_settings_path(project_root), quiet=True
|
|
676
|
+
)
|
|
677
|
+
ok_project_submit = install_user_prompt_submit_hook(
|
|
678
|
+
get_submit_settings_path(project_root), quiet=True
|
|
679
|
+
)
|
|
680
|
+
ok_project_permission = install_permission_request_hook(
|
|
681
|
+
get_permission_settings_path(project_root), quiet=True
|
|
682
|
+
)
|
|
717
683
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
684
|
+
all_hooks_ok = (
|
|
685
|
+
ok_global_stop
|
|
686
|
+
and ok_global_submit
|
|
687
|
+
and ok_global_permission
|
|
688
|
+
and ok_project_stop
|
|
689
|
+
and ok_project_submit
|
|
690
|
+
and ok_project_permission
|
|
691
|
+
)
|
|
692
|
+
if not all_hooks_ok:
|
|
693
|
+
self.app.notify(
|
|
694
|
+
"Claude hooks not fully installed; session id/title may not update",
|
|
695
|
+
title="Terminal",
|
|
696
|
+
severity="warning",
|
|
725
697
|
)
|
|
698
|
+
except Exception:
|
|
699
|
+
pass
|
|
726
700
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
701
|
+
command = self._command_in_directory(
|
|
702
|
+
tmux_manager.zsh_run_and_keep_open("claude"), workspace
|
|
703
|
+
)
|
|
704
|
+
created = tmux_manager.create_inner_window(
|
|
705
|
+
"cc",
|
|
706
|
+
tmux_manager.shell_command_with_env(command, env),
|
|
707
|
+
terminal_id=terminal_id,
|
|
708
|
+
provider="claude",
|
|
709
|
+
context_id=context_id,
|
|
710
|
+
)
|
|
711
|
+
if not created:
|
|
712
|
+
self.app.notify("Failed to open Claude terminal", title="Terminal", severity="error")
|
|
739
713
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
714
|
+
async def _create_codex_terminal(self, workspace: str) -> None:
|
|
715
|
+
"""Create a new Codex terminal."""
|
|
716
|
+
command = self._command_in_directory(
|
|
717
|
+
tmux_manager.zsh_run_and_keep_open("codex"), workspace
|
|
718
|
+
)
|
|
719
|
+
created = tmux_manager.create_inner_window("codex", command)
|
|
720
|
+
if not created:
|
|
721
|
+
self.app.notify("Failed to open Codex terminal", title="Terminal", severity="error")
|
|
722
|
+
|
|
723
|
+
async def _create_opencode_terminal(self, workspace: str) -> None:
|
|
724
|
+
"""Create a new Opencode terminal."""
|
|
725
|
+
command = self._command_in_directory(
|
|
726
|
+
tmux_manager.zsh_run_and_keep_open("opencode"), workspace
|
|
727
|
+
)
|
|
728
|
+
created = tmux_manager.create_inner_window("opencode", command)
|
|
729
|
+
if not created:
|
|
730
|
+
self.app.notify("Failed to open Opencode terminal", title="Terminal", severity="error")
|
|
754
731
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
732
|
+
async def _create_zsh_terminal(self, workspace: str) -> None:
|
|
733
|
+
"""Create a new zsh terminal."""
|
|
734
|
+
command = self._command_in_directory("zsh", workspace)
|
|
735
|
+
created = tmux_manager.create_inner_window("zsh", command)
|
|
736
|
+
if not created:
|
|
737
|
+
self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
|
|
759
738
|
|
|
760
|
-
|
|
761
|
-
|
|
739
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
740
|
+
button_id = event.button.id or ""
|
|
741
|
+
|
|
742
|
+
if not self.supported():
|
|
743
|
+
self.app.notify(
|
|
744
|
+
self._support_message(),
|
|
745
|
+
title="Terminal",
|
|
746
|
+
severity="warning",
|
|
762
747
|
)
|
|
763
|
-
created = tmux_manager.create_inner_window("codex", command)
|
|
764
|
-
if not created:
|
|
765
|
-
self.app.notify("Failed to open codex terminal", title="Terminal", severity="error")
|
|
766
|
-
await self.refresh_data()
|
|
767
748
|
return
|
|
768
749
|
|
|
769
|
-
if button_id == "new-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
await self.refresh_data()
|
|
750
|
+
if button_id == "new-agent":
|
|
751
|
+
from ..screens import CreateAgentScreen
|
|
752
|
+
|
|
753
|
+
self.app.push_screen(CreateAgentScreen(), self._on_create_agent_result)
|
|
774
754
|
return
|
|
775
755
|
|
|
776
756
|
if button_id.startswith("switch-"):
|
|
@@ -389,7 +389,9 @@ class WatcherPanel(Container, can_focus=True):
|
|
|
389
389
|
except Exception:
|
|
390
390
|
return []
|
|
391
391
|
|
|
392
|
-
def _collect_recent_sessions_page(
|
|
392
|
+
def _collect_recent_sessions_page(
|
|
393
|
+
self, *, page: int, rows_per_page: int
|
|
394
|
+
) -> tuple[list[dict], int]:
|
|
393
395
|
"""Collect one page of recent sessions from the database."""
|
|
394
396
|
try:
|
|
395
397
|
from ...db import get_database
|
|
@@ -477,7 +479,9 @@ class WatcherPanel(Container, can_focus=True):
|
|
|
477
479
|
if exists:
|
|
478
480
|
paths_lines.append(f" [green]●[/green] {name}: {path}")
|
|
479
481
|
else:
|
|
480
|
-
paths_lines.append(
|
|
482
|
+
paths_lines.append(
|
|
483
|
+
f" [yellow]○[/yellow] {name}: {path} [dim](not found)[/dim]"
|
|
484
|
+
)
|
|
481
485
|
else:
|
|
482
486
|
paths_lines.append(f" [dim]○ {name}: {path} (disabled)[/dim]")
|
|
483
487
|
paths_widget.update("\n".join(paths_lines))
|
|
@@ -483,7 +483,16 @@ class WorkerPanel(Container, can_focus=True):
|
|
|
483
483
|
("Failed", failed, "red"),
|
|
484
484
|
]:
|
|
485
485
|
bar_width = int((count / total) * max_width) if total > 0 else 0
|
|
486
|
-
bar =
|
|
486
|
+
bar = (
|
|
487
|
+
"["
|
|
488
|
+
+ color
|
|
489
|
+
+ "]"
|
|
490
|
+
+ ("█" * bar_width)
|
|
491
|
+
+ "[/"
|
|
492
|
+
+ color
|
|
493
|
+
+ "]"
|
|
494
|
+
+ ("░" * (max_width - bar_width))
|
|
495
|
+
)
|
|
487
496
|
lines.append(f" {label:<12} {bar} {count}")
|
|
488
497
|
|
|
489
498
|
return "\n".join(lines)
|
realign/db/__init__.py
CHANGED
|
@@ -20,7 +20,7 @@ def get_database(
|
|
|
20
20
|
blocking CLI commands under worker/watcher write load.
|
|
21
21
|
"""
|
|
22
22
|
global _DB_INSTANCE
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
# Resolution order:
|
|
25
25
|
# 1) Env override (tests/ops): REALIGN_SQLITE_DB_PATH or REALIGN_DB_PATH (legacy)
|
|
26
26
|
# 2) Config: ~/.aline/config.yaml (sqlite_db_path)
|
realign/db/base.py
CHANGED
|
@@ -121,9 +121,7 @@ class DatabaseInterface(ABC):
|
|
|
121
121
|
pass
|
|
122
122
|
|
|
123
123
|
@abstractmethod
|
|
124
|
-
def get_or_create_project(
|
|
125
|
-
self, path: Path, name: Optional[str] = None
|
|
126
|
-
) -> ProjectRecord:
|
|
124
|
+
def get_or_create_project(self, path: Path, name: Optional[str] = None) -> ProjectRecord:
|
|
127
125
|
"""Get existing project or create new one."""
|
|
128
126
|
pass
|
|
129
127
|
|
|
@@ -141,9 +139,7 @@ class DatabaseInterface(ABC):
|
|
|
141
139
|
pass
|
|
142
140
|
|
|
143
141
|
@abstractmethod
|
|
144
|
-
def update_session_activity(
|
|
145
|
-
self, session_id: str, last_activity_at: datetime
|
|
146
|
-
) -> None:
|
|
142
|
+
def update_session_activity(self, session_id: str, last_activity_at: datetime) -> None:
|
|
147
143
|
"""Update last activity timestamp for a session."""
|
|
148
144
|
pass
|
|
149
145
|
|
|
@@ -212,16 +208,12 @@ class DatabaseInterface(ABC):
|
|
|
212
208
|
pass
|
|
213
209
|
|
|
214
210
|
@abstractmethod
|
|
215
|
-
def get_turn_by_hash(
|
|
216
|
-
self, session_id: str, content_hash: str
|
|
217
|
-
) -> Optional[TurnRecord]:
|
|
211
|
+
def get_turn_by_hash(self, session_id: str, content_hash: str) -> Optional[TurnRecord]:
|
|
218
212
|
"""Check if a turn with this content hash already exists in the session."""
|
|
219
213
|
pass
|
|
220
214
|
|
|
221
215
|
@abstractmethod
|
|
222
|
-
def get_turn_by_number(
|
|
223
|
-
self, session_id: str, turn_number: int
|
|
224
|
-
) -> Optional[TurnRecord]:
|
|
216
|
+
def get_turn_by_number(self, session_id: str, turn_number: int) -> Optional[TurnRecord]:
|
|
225
217
|
"""Get a turn by session_id and turn_number."""
|
|
226
218
|
pass
|
|
227
219
|
|
|
@@ -379,9 +371,7 @@ class DatabaseInterface(ABC):
|
|
|
379
371
|
pass
|
|
380
372
|
|
|
381
373
|
@abstractmethod
|
|
382
|
-
def try_acquire_lock(
|
|
383
|
-
self, lock_key: str, *, owner: str, ttl_seconds: float
|
|
384
|
-
) -> bool:
|
|
374
|
+
def try_acquire_lock(self, lock_key: str, *, owner: str, ttl_seconds: float) -> bool:
|
|
385
375
|
"""Try to acquire a cross-process lease lock."""
|
|
386
376
|
pass
|
|
387
377
|
|
realign/db/locks.py
CHANGED