aline-ai 0.5.5__py3-none-any.whl → 0.5.7__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.5.dist-info → aline_ai-0.5.7.dist-info}/METADATA +1 -1
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/RECORD +18 -16
- realign/__init__.py +1 -1
- realign/adapters/claude.py +13 -7
- realign/cli.py +16 -4
- realign/commands/init.py +31 -5
- realign/dashboard/app.py +32 -22
- realign/dashboard/screens/__init__.py +10 -1
- realign/dashboard/screens/create_agent.py +244 -0
- realign/dashboard/screens/help_screen.py +114 -0
- realign/dashboard/widgets/events_table.py +311 -69
- realign/dashboard/widgets/header.py +1 -1
- realign/dashboard/widgets/sessions_table.py +380 -70
- realign/dashboard/widgets/terminal_panel.py +132 -196
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.5.dist-info → aline_ai-0.5.7.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
aline_ai-0.5.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.5.7.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=yVsHlHyZpDRPyaFzYXTltDHKp6XXpiIGe-TlrznKALo,1623
|
|
3
3
|
realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
|
|
4
|
-
realign/cli.py,sha256=
|
|
4
|
+
realign/cli.py,sha256=yeq_a3Peoqx8N13Jo2etjJtbTCZYpuqwoMMyAPdrANs,30569
|
|
5
5
|
realign/codex_detector.py,sha256=N9ulgMgvTzDfXE4s4vLd6OoS0hT7R6h2bDFFXWa-2hE,4183
|
|
6
6
|
realign/config.py,sha256=lIKZqeOwYc_gHo760lYYX6PnapuKrCWGqT5SA8-PbeA,12044
|
|
7
7
|
realign/context.py,sha256=S1YEUn5HWSDTerDDMsSsRV871IZxoaxDjPTPI2z6-Xs,9976
|
|
@@ -19,7 +19,7 @@ realign/worker_daemon.py,sha256=LpJbQDY0Z4AMtq0LmpxvFeQM4puuoGDRBayKRafvKhc,3574
|
|
|
19
19
|
realign/adapters/__init__.py,sha256=bpDm5aBxMdq4OA_beYahoUb4zfNaq3KOG6KghQJruRc,827
|
|
20
20
|
realign/adapters/antigravity.py,sha256=geaYxAEswpgsVtERqsQ1OwvPFsy5tRkyjx2yQ-Uq9nM,5461
|
|
21
21
|
realign/adapters/base.py,sha256=2IdAZKGjg5gPB3YLf_8r3V4XAdbK7fHpj06GjjsYEFY,7409
|
|
22
|
-
realign/adapters/claude.py,sha256=
|
|
22
|
+
realign/adapters/claude.py,sha256=ksTRwC5Z8AzUcB21LFjx6DETP08cv__fjgBzm-TeZdI,5444
|
|
23
23
|
realign/adapters/codex.py,sha256=5ex3zJ5Hpb_StV2CcBSHVhHleygZxzVJjYsWw8qK1Bc,2051
|
|
24
24
|
realign/adapters/gemini.py,sha256=NvtXQPWUtEY-DaAAMvLGvQW4FalTG-g0pD514HYnzF0,2540
|
|
25
25
|
realign/adapters/registry.py,sha256=yM6nf9nGTJ1vaK2Uixp-VacseK7PmxZkCdKedmWI8MA,3255
|
|
@@ -37,29 +37,31 @@ realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,67
|
|
|
37
37
|
realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
|
|
38
38
|
realign/commands/export_shares.py,sha256=Djy1aO7MoU1_ewzn6CZ43oNhSEEonV3sTkSQbHgiaKI,135806
|
|
39
39
|
realign/commands/import_shares.py,sha256=ukX8huvLvEM5g0qEIoqrV1-imz1g-r0Jj2FqD-ojrIA,25297
|
|
40
|
-
realign/commands/init.py,sha256=
|
|
40
|
+
realign/commands/init.py,sha256=ef-q3Qz5D_0Eqld8qjtX26X2QrovBSYcva3uAjiJuwk,33015
|
|
41
41
|
realign/commands/restore.py,sha256=s2BxQZHxQw9r12NzRVsK20KlGafy5AIoSjWMo5PcnHY,11173
|
|
42
42
|
realign/commands/search.py,sha256=RUdseQsjy-SNfKFkGLWrE4IhxkzgkW9IIxAX33XnCHk,24589
|
|
43
43
|
realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
|
|
44
44
|
realign/commands/watcher.py,sha256=fWL3kaRkqE03-NtFLaXlx93hJAQrAuNPSoYhOyQZfq8,136273
|
|
45
45
|
realign/commands/worker.py,sha256=K1DG1uZ--ebKwklHCyIFdN_axoLjL9Onx8Naq-DOZBs,23078
|
|
46
46
|
realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
|
|
47
|
-
realign/dashboard/app.py,sha256=
|
|
47
|
+
realign/dashboard/app.py,sha256=LPgDXCqXHtJXzgFiwJvWolLJZ34EU4gejLrBIrgEk4Y,12233
|
|
48
48
|
realign/dashboard/tmux_manager.py,sha256=DdCiumQ7YQZnje5VfOQ60585C0X6Va_AhBQi_zmhE0Y,24035
|
|
49
|
-
realign/dashboard/screens/__init__.py,sha256=
|
|
49
|
+
realign/dashboard/screens/__init__.py,sha256=US6sAmQs5VVkH2tFkH_z0WDT4H8cVhLL-JckfSR1yQY,446
|
|
50
|
+
realign/dashboard/screens/create_agent.py,sha256=ugEs3IHrT7FsbuMEwyrqY3eoylp_pbftw42_Fu07tF4,7419
|
|
50
51
|
realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
|
|
51
52
|
realign/dashboard/screens/event_detail.py,sha256=OLaL3-FgAohDdzVlfuUw5yh2SR49IHIpCtiqXJhBTc0,20992
|
|
53
|
+
realign/dashboard/screens/help_screen.py,sha256=Icrcvbgyz49R2tBiu8vBZ4CLm6iYclv_-FTa2pCFRRQ,3398
|
|
52
54
|
realign/dashboard/screens/session_detail.py,sha256=gfpUIhMO00ecMlMyzpkxDdvGb9zhESEvxwrJvqLuHOI,9603
|
|
53
55
|
realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
|
|
54
56
|
realign/dashboard/styles/dashboard.tcss,sha256=9sSIs3r4V8eeTwCK56s7fnYxjMEuASP8EcmK1fhpUmA,3454
|
|
55
57
|
realign/dashboard/widgets/__init__.py,sha256=3Pf2_K9obrertgv_psfxradgkI9RXlmjoXYQH7oBKm0,583
|
|
56
58
|
realign/dashboard/widgets/config_panel.py,sha256=Afezfd6nvHo0Q44IS2UZTPJsYmHbqzjx7bi5jWrCDPA,11182
|
|
57
|
-
realign/dashboard/widgets/events_table.py,sha256=
|
|
58
|
-
realign/dashboard/widgets/header.py,sha256=
|
|
59
|
+
realign/dashboard/widgets/events_table.py,sha256=OG9RjwU4c50-RUMmdhXzmIMnYrt6_mCP1GNQWDAX95s,30368
|
|
60
|
+
realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
|
|
59
61
|
realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
|
|
60
62
|
realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
|
|
61
|
-
realign/dashboard/widgets/sessions_table.py,sha256=
|
|
62
|
-
realign/dashboard/widgets/terminal_panel.py,sha256=
|
|
63
|
+
realign/dashboard/widgets/sessions_table.py,sha256=toHE96RLwddqXE9Ykocy6loqoGld_6gFawLwdiiJ2cA,32877
|
|
64
|
+
realign/dashboard/widgets/terminal_panel.py,sha256=uXgPcgjWaQ2tTD6Mx6ikCXzq6wYqh-ft0Bait83_DKE,28290
|
|
63
65
|
realign/dashboard/widgets/watcher_panel.py,sha256=O_mdDacgc87xA-5KEfta53Ik_Xsk_B2OfwenMOTtGw8,19722
|
|
64
66
|
realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
|
|
65
67
|
realign/db/__init__.py,sha256=-1d-Zc4IOUVokbdTXi3R-bIwlkFEPAz_qTHAdcsdp6g,1870
|
|
@@ -86,8 +88,8 @@ realign/triggers/next_turn_trigger.py,sha256=BpP0PWn4mU1MZd6mv89jWcjs8Jtv0zEWapW
|
|
|
86
88
|
realign/triggers/registry.py,sha256=cb-AVLbYB2pqwfWL3q1DQxLv4kOw7g7m-GshTdfFESc,3827
|
|
87
89
|
realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
|
|
88
90
|
realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
|
|
89
|
-
aline_ai-0.5.
|
|
90
|
-
aline_ai-0.5.
|
|
91
|
-
aline_ai-0.5.
|
|
92
|
-
aline_ai-0.5.
|
|
93
|
-
aline_ai-0.5.
|
|
91
|
+
aline_ai-0.5.7.dist-info/METADATA,sha256=SG0PLhxEgWQIrjE3rCiSk9DFURyfEkxXteRKqdFhijU,1597
|
|
92
|
+
aline_ai-0.5.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
93
|
+
aline_ai-0.5.7.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
94
|
+
aline_ai-0.5.7.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
95
|
+
aline_ai-0.5.7.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
realign/adapters/claude.py
CHANGED
|
@@ -76,14 +76,13 @@ class ClaudeAdapter(SessionAdapter):
|
|
|
76
76
|
project_path_str = "/" + parent_name[1:].replace("-", "/")
|
|
77
77
|
project_path = Path(project_path_str)
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
# We relax this slightly: if it's a valid-looking path, we return it,
|
|
81
|
-
# but ideally we check existence.
|
|
79
|
+
# If path exists locally, return it immediately
|
|
82
80
|
if project_path.exists():
|
|
83
81
|
return project_path
|
|
84
82
|
|
|
85
83
|
# Fallback: read cwd from the session JSONL (more reliable when the encoded
|
|
86
84
|
# directory name is ambiguous due to '-' in real paths).
|
|
85
|
+
cwd_path: Optional[Path] = None
|
|
87
86
|
try:
|
|
88
87
|
with session_file.open("r", encoding="utf-8") as f:
|
|
89
88
|
for i, line in enumerate(f):
|
|
@@ -98,13 +97,20 @@ class ClaudeAdapter(SessionAdapter):
|
|
|
98
97
|
continue
|
|
99
98
|
cwd = obj.get("cwd")
|
|
100
99
|
if isinstance(cwd, str) and cwd.strip():
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
cwd_path = Path(cwd.strip())
|
|
101
|
+
# If cwd exists locally, prefer it
|
|
102
|
+
if cwd_path.exists():
|
|
103
|
+
return cwd_path
|
|
104
|
+
break # Found cwd, stop searching
|
|
104
105
|
except Exception:
|
|
105
106
|
pass
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
# Return the best available path even if it doesn't exist locally.
|
|
109
|
+
# This allows importing sessions from other machines (e.g., SWEBench).
|
|
110
|
+
# Prefer cwd from JSONL as it's more accurate than path decoding.
|
|
111
|
+
if cwd_path is not None:
|
|
112
|
+
return cwd_path
|
|
113
|
+
return project_path
|
|
108
114
|
|
|
109
115
|
except Exception:
|
|
110
116
|
return None
|
realign/cli.py
CHANGED
|
@@ -19,12 +19,19 @@ console = Console()
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@app.callback()
|
|
22
|
-
def main(
|
|
22
|
+
def main(
|
|
23
|
+
ctx: typer.Context,
|
|
24
|
+
dev: bool = typer.Option(False, "--dev", help="Enable developer mode (shows Watcher and Worker tabs)"),
|
|
25
|
+
):
|
|
23
26
|
"""
|
|
24
27
|
Aline CLI - Shared AI Memory for teams.
|
|
25
28
|
|
|
26
29
|
Run 'aline' without arguments to open the interactive dashboard.
|
|
27
30
|
"""
|
|
31
|
+
# Store dev mode in context for subcommands
|
|
32
|
+
ctx.ensure_object(dict)
|
|
33
|
+
ctx.obj["dev"] = dev
|
|
34
|
+
|
|
28
35
|
if ctx.invoked_subcommand is None:
|
|
29
36
|
# Check for updates before launching dashboard
|
|
30
37
|
from .commands.upgrade import check_and_prompt_update
|
|
@@ -40,7 +47,7 @@ def main(ctx: typer.Context):
|
|
|
40
47
|
|
|
41
48
|
from .dashboard.app import AlineDashboard
|
|
42
49
|
|
|
43
|
-
dashboard = AlineDashboard()
|
|
50
|
+
dashboard = AlineDashboard(dev_mode=dev)
|
|
44
51
|
dashboard.run()
|
|
45
52
|
|
|
46
53
|
|
|
@@ -809,7 +816,10 @@ def version():
|
|
|
809
816
|
|
|
810
817
|
|
|
811
818
|
@app.command()
|
|
812
|
-
def dashboard(
|
|
819
|
+
def dashboard(
|
|
820
|
+
ctx: typer.Context,
|
|
821
|
+
dev: bool = typer.Option(False, "--dev", help="Enable developer mode (shows Watcher and Worker tabs)"),
|
|
822
|
+
):
|
|
813
823
|
"""Open the interactive TUI dashboard."""
|
|
814
824
|
from .dashboard.tmux_manager import bootstrap_dashboard_into_tmux
|
|
815
825
|
|
|
@@ -817,7 +827,9 @@ def dashboard():
|
|
|
817
827
|
|
|
818
828
|
from .dashboard.app import AlineDashboard
|
|
819
829
|
|
|
820
|
-
|
|
830
|
+
# Use dev flag from this command or inherit from parent context
|
|
831
|
+
dev_mode = dev or (ctx.obj.get("dev", False) if ctx.obj else False)
|
|
832
|
+
dash = AlineDashboard(dev_mode=dev_mode)
|
|
821
833
|
dash.run()
|
|
822
834
|
|
|
823
835
|
|
realign/commands/init.py
CHANGED
|
@@ -18,10 +18,14 @@ console = Console()
|
|
|
18
18
|
|
|
19
19
|
# tmux config template for Aline-managed dashboard sessions.
|
|
20
20
|
# Stored at ~/.aline/tmux/tmux.conf and sourced by the dashboard tmux bootstrap.
|
|
21
|
+
# Bump this version when the tmux config changes to trigger auto-update on `aline init`.
|
|
22
|
+
_TMUX_CONFIG_VERSION = 2
|
|
23
|
+
|
|
24
|
+
|
|
21
25
|
def _get_tmux_config() -> str:
|
|
22
26
|
"""Generate tmux config with Type-to-Exit bindings."""
|
|
23
|
-
conf =
|
|
24
|
-
#
|
|
27
|
+
conf = f"# Aline tmux config (v{_TMUX_CONFIG_VERSION})\n"
|
|
28
|
+
conf += r"""#
|
|
25
29
|
# Goal: make mouse selection copy to the system clipboard (macOS Terminal friendly).
|
|
26
30
|
# - Drag-select text with the mouse; when you release, it is copied to the clipboard.
|
|
27
31
|
# - Paste anywhere with Cmd+V.
|
|
@@ -448,24 +452,46 @@ def _initialize_prompts_directory() -> None:
|
|
|
448
452
|
file_path.write_text(content, encoding="utf-8")
|
|
449
453
|
|
|
450
454
|
|
|
455
|
+
def _get_tmux_config_version(content: str) -> int:
|
|
456
|
+
"""Extract version number from tmux config content. Returns 0 if not found."""
|
|
457
|
+
# Look for "# Aline tmux config (vN)" pattern
|
|
458
|
+
match = re.search(r"# Aline tmux config \(v(\d+)\)", content)
|
|
459
|
+
if match:
|
|
460
|
+
return int(match.group(1))
|
|
461
|
+
# Old configs without version marker are version 1
|
|
462
|
+
if "# Aline tmux config" in content:
|
|
463
|
+
return 1
|
|
464
|
+
return 0
|
|
465
|
+
|
|
466
|
+
|
|
451
467
|
def _initialize_tmux_config() -> Path:
|
|
452
|
-
"""Initialize ~/.aline/tmux/tmux.conf
|
|
468
|
+
"""Initialize ~/.aline/tmux/tmux.conf with auto-update on version change."""
|
|
453
469
|
tmux_conf_path = Path.home() / ".aline" / "tmux" / "tmux.conf"
|
|
454
470
|
tmux_conf_path.parent.mkdir(parents=True, exist_ok=True)
|
|
471
|
+
|
|
455
472
|
if not tmux_conf_path.exists():
|
|
456
473
|
tmux_conf_path.write_text(_get_tmux_config(), encoding="utf-8")
|
|
457
474
|
return tmux_conf_path
|
|
458
475
|
|
|
459
|
-
#
|
|
460
|
-
# tmux parses `#` as a comment delimiter, turning `bind ... # ...` into `bind ...` (invalid).
|
|
476
|
+
# Check existing config
|
|
461
477
|
try:
|
|
462
478
|
existing = tmux_conf_path.read_text(encoding="utf-8")
|
|
463
479
|
except Exception:
|
|
464
480
|
return tmux_conf_path
|
|
465
481
|
|
|
482
|
+
# Only manage Aline-generated configs
|
|
466
483
|
if "# Aline tmux config" not in existing:
|
|
467
484
|
return tmux_conf_path
|
|
468
485
|
|
|
486
|
+
# Check version and update if outdated
|
|
487
|
+
existing_version = _get_tmux_config_version(existing)
|
|
488
|
+
if existing_version < _TMUX_CONFIG_VERSION:
|
|
489
|
+
# Auto-update to latest config
|
|
490
|
+
tmux_conf_path.write_text(_get_tmux_config(), encoding="utf-8")
|
|
491
|
+
return tmux_conf_path
|
|
492
|
+
|
|
493
|
+
# Best-effort repair for older Aline-generated configs that used unquoted `#` keys.
|
|
494
|
+
# tmux parses `#` as a comment delimiter, turning `bind ... # ...` into `bind ...` (invalid).
|
|
469
495
|
repaired = existing
|
|
470
496
|
repaired = _TMUX_CONF_REPAIR_TILDE_KEY_RE.sub(r"\1\\~\2\\~", repaired)
|
|
471
497
|
repaired = _TMUX_CONF_REPAIR_KEY_NEEDS_QUOTE_RE.sub(r'\1"\2"\3"\2"', repaired)
|
realign/dashboard/app.py
CHANGED
|
@@ -54,35 +54,45 @@ class AlineDashboard(App):
|
|
|
54
54
|
TITLE = "Aline Dashboard"
|
|
55
55
|
|
|
56
56
|
BINDINGS = [
|
|
57
|
-
Binding("r", "refresh", "Refresh"),
|
|
57
|
+
Binding("r", "refresh", "Refresh", show=False),
|
|
58
58
|
Binding("?", "help", "Help"),
|
|
59
|
-
Binding("tab", "next_tab", "Next Tab", priority=True),
|
|
60
|
-
Binding("shift+tab", "prev_tab", "Prev Tab", priority=True),
|
|
61
|
-
Binding("n", "page_next", "Next Page"),
|
|
62
|
-
Binding("p", "page_prev", "Prev Page"),
|
|
63
|
-
Binding("s", "switch_view", "Switch View"),
|
|
64
|
-
Binding("c", "create_event", "Create Event"),
|
|
65
|
-
Binding("l", "load_context", "Load Context"),
|
|
66
|
-
Binding("y", "share_import", "Share Import"),
|
|
59
|
+
Binding("tab", "next_tab", "Next Tab", priority=True, show=False),
|
|
60
|
+
Binding("shift+tab", "prev_tab", "Prev Tab", priority=True, show=False),
|
|
61
|
+
Binding("n", "page_next", "Next Page", show=False),
|
|
62
|
+
Binding("p", "page_prev", "Prev Page", show=False),
|
|
63
|
+
Binding("s", "switch_view", "Switch View", show=False),
|
|
64
|
+
Binding("c", "create_event", "Create Event", show=False),
|
|
65
|
+
Binding("l", "load_context", "Load Context", show=False),
|
|
66
|
+
Binding("y", "share_import", "Share Import", show=False),
|
|
67
67
|
Binding("ctrl+c", "quit_confirm", "Quit", priority=True),
|
|
68
68
|
]
|
|
69
69
|
|
|
70
70
|
_quit_confirm_window_s: float = 1.2
|
|
71
71
|
|
|
72
|
+
def __init__(self, dev_mode: bool = False):
|
|
73
|
+
"""Initialize the dashboard.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
dev_mode: If True, shows developer tabs (Watcher, Worker).
|
|
77
|
+
"""
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.dev_mode = dev_mode
|
|
80
|
+
|
|
72
81
|
def compose(self) -> ComposeResult:
|
|
73
82
|
"""Compose the dashboard layout."""
|
|
74
83
|
yield AlineHeader()
|
|
75
84
|
tab_ids = self._tab_ids()
|
|
76
85
|
with TabbedContent(initial=tab_ids[0] if tab_ids else "terminal"):
|
|
77
|
-
with TabPane("
|
|
86
|
+
with TabPane("Agents", id="terminal"):
|
|
78
87
|
yield TerminalPanel()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
if self.dev_mode:
|
|
89
|
+
with TabPane("Watcher", id="watcher"):
|
|
90
|
+
yield WatcherPanel()
|
|
91
|
+
with TabPane("Worker", id="worker"):
|
|
92
|
+
yield WorkerPanel()
|
|
93
|
+
with TabPane("Contexts", id="sessions"):
|
|
84
94
|
yield SessionsTable()
|
|
85
|
-
with TabPane("
|
|
95
|
+
with TabPane("Share", id="events"):
|
|
86
96
|
yield EventsTable()
|
|
87
97
|
with TabPane("Config", id="config"):
|
|
88
98
|
yield ConfigPanel()
|
|
@@ -91,7 +101,9 @@ class AlineDashboard(App):
|
|
|
91
101
|
yield Footer()
|
|
92
102
|
|
|
93
103
|
def _tab_ids(self) -> list[str]:
|
|
94
|
-
|
|
104
|
+
if self.dev_mode:
|
|
105
|
+
return ["terminal", "watcher", "worker", "sessions", "events", "config", "search"]
|
|
106
|
+
return ["terminal", "sessions", "events", "config", "search"]
|
|
95
107
|
|
|
96
108
|
def on_mount(self) -> None:
|
|
97
109
|
"""Apply theme based on system settings and watch for changes."""
|
|
@@ -190,11 +202,9 @@ class AlineDashboard(App):
|
|
|
190
202
|
|
|
191
203
|
def action_help(self) -> None:
|
|
192
204
|
"""Show help information."""
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
timeout=5,
|
|
197
|
-
)
|
|
205
|
+
from .screens import HelpScreen
|
|
206
|
+
|
|
207
|
+
self.push_screen(HelpScreen())
|
|
198
208
|
|
|
199
209
|
def action_quit_confirm(self) -> None:
|
|
200
210
|
"""Quit only when Ctrl+C is pressed twice quickly."""
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
from .session_detail import SessionDetailScreen
|
|
4
4
|
from .event_detail import EventDetailScreen
|
|
5
5
|
from .create_event import CreateEventScreen
|
|
6
|
+
from .create_agent import CreateAgentScreen
|
|
6
7
|
from .share_import import ShareImportScreen
|
|
8
|
+
from .help_screen import HelpScreen
|
|
7
9
|
|
|
8
|
-
__all__ = [
|
|
10
|
+
__all__ = [
|
|
11
|
+
"SessionDetailScreen",
|
|
12
|
+
"EventDetailScreen",
|
|
13
|
+
"CreateEventScreen",
|
|
14
|
+
"CreateAgentScreen",
|
|
15
|
+
"ShareImportScreen",
|
|
16
|
+
"HelpScreen",
|
|
17
|
+
]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Create agent modal for the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from textual.app import ComposeResult
|
|
13
|
+
from textual.binding import Binding
|
|
14
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
15
|
+
from textual.screen import ModalScreen
|
|
16
|
+
from textual.widgets import Button, Label, RadioButton, RadioSet, Static
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# State file for storing last workspace path
|
|
20
|
+
DASHBOARD_STATE_FILE = Path.home() / ".aline" / "dashboard_state.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_last_workspace() -> str:
|
|
24
|
+
"""Load the last used workspace path from state file."""
|
|
25
|
+
try:
|
|
26
|
+
if DASHBOARD_STATE_FILE.exists():
|
|
27
|
+
with open(DASHBOARD_STATE_FILE, "r", encoding="utf-8") as f:
|
|
28
|
+
state = json.load(f)
|
|
29
|
+
path = state.get("last_workspace", "")
|
|
30
|
+
if path and os.path.isdir(path):
|
|
31
|
+
return path
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
# Default to current working directory or home
|
|
35
|
+
try:
|
|
36
|
+
return os.getcwd()
|
|
37
|
+
except Exception:
|
|
38
|
+
return str(Path.home())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save_last_workspace(path: str) -> None:
|
|
42
|
+
"""Save the last used workspace path to state file."""
|
|
43
|
+
try:
|
|
44
|
+
DASHBOARD_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
state = {}
|
|
46
|
+
if DASHBOARD_STATE_FILE.exists():
|
|
47
|
+
with open(DASHBOARD_STATE_FILE, "r", encoding="utf-8") as f:
|
|
48
|
+
state = json.load(f)
|
|
49
|
+
state["last_workspace"] = path
|
|
50
|
+
with open(DASHBOARD_STATE_FILE, "w", encoding="utf-8") as f:
|
|
51
|
+
json.dump(state, f, indent=2)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CreateAgentScreen(ModalScreen[Optional[tuple[str, str]]]):
|
|
57
|
+
"""Modal to create a new agent terminal.
|
|
58
|
+
|
|
59
|
+
Returns a tuple of (agent_type, workspace_path) on success, None on cancel.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
BINDINGS = [
|
|
63
|
+
Binding("escape", "close", "Close", show=False),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
DEFAULT_CSS = """
|
|
67
|
+
CreateAgentScreen {
|
|
68
|
+
align: center middle;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
CreateAgentScreen #create-agent-root {
|
|
72
|
+
width: 60;
|
|
73
|
+
height: auto;
|
|
74
|
+
max-height: 80%;
|
|
75
|
+
padding: 1 2;
|
|
76
|
+
background: $background;
|
|
77
|
+
border: solid $accent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
CreateAgentScreen #create-agent-title {
|
|
81
|
+
height: auto;
|
|
82
|
+
margin-bottom: 1;
|
|
83
|
+
text-style: bold;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
CreateAgentScreen .section-label {
|
|
87
|
+
height: auto;
|
|
88
|
+
margin-top: 1;
|
|
89
|
+
margin-bottom: 0;
|
|
90
|
+
color: $text-muted;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
CreateAgentScreen RadioSet {
|
|
94
|
+
height: auto;
|
|
95
|
+
margin: 0;
|
|
96
|
+
padding: 0;
|
|
97
|
+
border: none;
|
|
98
|
+
background: transparent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
CreateAgentScreen RadioButton {
|
|
102
|
+
height: auto;
|
|
103
|
+
padding: 0;
|
|
104
|
+
margin: 0;
|
|
105
|
+
background: transparent;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
CreateAgentScreen #workspace-section {
|
|
109
|
+
height: auto;
|
|
110
|
+
margin-top: 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
CreateAgentScreen #workspace-row {
|
|
114
|
+
height: auto;
|
|
115
|
+
margin-top: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
CreateAgentScreen #workspace-path {
|
|
119
|
+
width: 1fr;
|
|
120
|
+
height: auto;
|
|
121
|
+
padding: 0 1;
|
|
122
|
+
background: $surface;
|
|
123
|
+
border: solid $primary-lighten-2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
CreateAgentScreen #browse-btn {
|
|
127
|
+
width: auto;
|
|
128
|
+
min-width: 10;
|
|
129
|
+
margin-left: 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
CreateAgentScreen #buttons {
|
|
133
|
+
height: auto;
|
|
134
|
+
margin-top: 2;
|
|
135
|
+
align: right middle;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
CreateAgentScreen #buttons Button {
|
|
139
|
+
margin-left: 1;
|
|
140
|
+
}
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self) -> None:
|
|
144
|
+
super().__init__()
|
|
145
|
+
self._workspace_path = _load_last_workspace()
|
|
146
|
+
|
|
147
|
+
def compose(self) -> ComposeResult:
|
|
148
|
+
with Container(id="create-agent-root"):
|
|
149
|
+
yield Static("Create New Agent", id="create-agent-title")
|
|
150
|
+
|
|
151
|
+
yield Label("Agent Type", classes="section-label")
|
|
152
|
+
with RadioSet(id="agent-type"):
|
|
153
|
+
yield RadioButton("Claude", id="type-claude", value=True)
|
|
154
|
+
yield RadioButton("Codex", id="type-codex")
|
|
155
|
+
yield RadioButton("Opencode", id="type-opencode")
|
|
156
|
+
yield RadioButton("zsh", id="type-zsh")
|
|
157
|
+
|
|
158
|
+
with Vertical(id="workspace-section"):
|
|
159
|
+
yield Label("Workspace", classes="section-label")
|
|
160
|
+
with Horizontal(id="workspace-row"):
|
|
161
|
+
yield Static(self._workspace_path, id="workspace-path")
|
|
162
|
+
yield Button("Browse", id="browse-btn", variant="default")
|
|
163
|
+
|
|
164
|
+
with Horizontal(id="buttons"):
|
|
165
|
+
yield Button("Cancel", id="cancel")
|
|
166
|
+
yield Button("Create", id="create", variant="primary")
|
|
167
|
+
|
|
168
|
+
def on_mount(self) -> None:
|
|
169
|
+
self.query_one("#create", Button).focus()
|
|
170
|
+
|
|
171
|
+
def action_close(self) -> None:
|
|
172
|
+
self.dismiss(None)
|
|
173
|
+
|
|
174
|
+
def _update_workspace_display(self) -> None:
|
|
175
|
+
"""Update the workspace path display."""
|
|
176
|
+
self.query_one("#workspace-path", Static).update(self._workspace_path)
|
|
177
|
+
|
|
178
|
+
async def _select_workspace(self) -> str | None:
|
|
179
|
+
"""Open macOS folder picker and return selected path, or None if cancelled."""
|
|
180
|
+
default_path = self._workspace_path
|
|
181
|
+
default_path_escaped = default_path.replace('"', '\\"')
|
|
182
|
+
prompt = "Select workspace folder"
|
|
183
|
+
prompt_escaped = prompt.replace('"', '\\"')
|
|
184
|
+
script = f"""
|
|
185
|
+
set defaultFolder to POSIX file "{default_path_escaped}" as alias
|
|
186
|
+
try
|
|
187
|
+
set selectedFolder to choose folder with prompt "{prompt_escaped}" default location defaultFolder
|
|
188
|
+
return POSIX path of selectedFolder
|
|
189
|
+
on error
|
|
190
|
+
return ""
|
|
191
|
+
end try
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
proc = await asyncio.get_event_loop().run_in_executor(
|
|
195
|
+
None,
|
|
196
|
+
lambda: subprocess.run(
|
|
197
|
+
["osascript", "-e", script],
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
timeout=120,
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
result = (proc.stdout or "").strip()
|
|
204
|
+
if result and os.path.isdir(result):
|
|
205
|
+
return result
|
|
206
|
+
return None
|
|
207
|
+
except Exception:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
211
|
+
button_id = event.button.id or ""
|
|
212
|
+
|
|
213
|
+
if button_id == "cancel":
|
|
214
|
+
self.dismiss(None)
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
if button_id == "browse-btn":
|
|
218
|
+
new_path = await self._select_workspace()
|
|
219
|
+
if new_path:
|
|
220
|
+
self._workspace_path = new_path
|
|
221
|
+
self._update_workspace_display()
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
if button_id == "create":
|
|
225
|
+
# Get selected agent type
|
|
226
|
+
radio_set = self.query_one("#agent-type", RadioSet)
|
|
227
|
+
pressed_button = radio_set.pressed_button
|
|
228
|
+
if pressed_button is None:
|
|
229
|
+
self.app.notify("Please select an agent type", severity="warning")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
agent_type_map = {
|
|
233
|
+
"type-claude": "claude",
|
|
234
|
+
"type-codex": "codex",
|
|
235
|
+
"type-opencode": "opencode",
|
|
236
|
+
"type-zsh": "zsh",
|
|
237
|
+
}
|
|
238
|
+
agent_type = agent_type_map.get(pressed_button.id or "", "claude")
|
|
239
|
+
|
|
240
|
+
# Save the workspace path for next time
|
|
241
|
+
_save_last_workspace(self._workspace_path)
|
|
242
|
+
|
|
243
|
+
# Return the result
|
|
244
|
+
self.dismiss((agent_type, self._workspace_path))
|