aline-ai 0.6.4__py3-none-any.whl → 0.6.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.6.4.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/RECORD +41 -34
- realign/__init__.py +1 -1
- realign/agent_names.py +79 -0
- realign/claude_hooks/stop_hook.py +3 -0
- realign/claude_hooks/terminal_state.py +11 -0
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +1 -1
- realign/codex_home.py +46 -15
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/doctor.py +3 -1
- realign/commands/export_shares.py +297 -0
- realign/commands/search.py +58 -29
- realign/dashboard/app.py +9 -158
- realign/dashboard/clipboard.py +54 -0
- realign/dashboard/screens/__init__.py +4 -0
- realign/dashboard/screens/agent_detail.py +333 -0
- realign/dashboard/screens/create_agent_info.py +133 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +67 -0
- realign/dashboard/tmux_manager.py +49 -8
- realign/dashboard/widgets/__init__.py +2 -0
- realign/dashboard/widgets/agents_panel.py +1129 -0
- realign/dashboard/widgets/config_panel.py +17 -11
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/dashboard/widgets/terminal_panel.py +109 -31
- realign/db/base.py +27 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +53 -2
- realign/db/sqlite_db.py +185 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +60 -3
- realign/worker_core.py +24 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Create agent info modal for the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual.containers import Container, Horizontal
|
|
11
|
+
from textual.screen import ModalScreen
|
|
12
|
+
from textual.widgets import Button, Input, Label, Static
|
|
13
|
+
|
|
14
|
+
from ...logging_config import setup_logger
|
|
15
|
+
|
|
16
|
+
logger = setup_logger("realign.dashboard.screens.create_agent_info", "dashboard.log")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateAgentInfoScreen(ModalScreen[Optional[dict]]):
|
|
20
|
+
"""Modal to create a new agent profile.
|
|
21
|
+
|
|
22
|
+
Returns a dict with agent info on success, None on cancel.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
BINDINGS = [
|
|
26
|
+
Binding("escape", "close", "Close", show=False),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DEFAULT_CSS = """
|
|
30
|
+
CreateAgentInfoScreen {
|
|
31
|
+
align: center middle;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
CreateAgentInfoScreen #create-agent-info-root {
|
|
35
|
+
width: 60;
|
|
36
|
+
height: auto;
|
|
37
|
+
max-height: 80%;
|
|
38
|
+
padding: 1 2;
|
|
39
|
+
background: $background;
|
|
40
|
+
border: solid $accent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
CreateAgentInfoScreen #create-agent-info-title {
|
|
44
|
+
height: auto;
|
|
45
|
+
margin-bottom: 1;
|
|
46
|
+
text-style: bold;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
CreateAgentInfoScreen .section-label {
|
|
50
|
+
height: auto;
|
|
51
|
+
margin-top: 1;
|
|
52
|
+
margin-bottom: 0;
|
|
53
|
+
color: $text-muted;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
CreateAgentInfoScreen Input {
|
|
57
|
+
height: auto;
|
|
58
|
+
margin-top: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
CreateAgentInfoScreen #buttons {
|
|
62
|
+
height: auto;
|
|
63
|
+
margin-top: 2;
|
|
64
|
+
align: right middle;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
CreateAgentInfoScreen #buttons Button {
|
|
68
|
+
margin-left: 1;
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def compose(self) -> ComposeResult:
|
|
73
|
+
with Container(id="create-agent-info-root"):
|
|
74
|
+
yield Static("Create Agent Profile", id="create-agent-info-title")
|
|
75
|
+
|
|
76
|
+
yield Label("Name", classes="section-label")
|
|
77
|
+
yield Input(placeholder="Agent name (leave blank for random)", id="agent-name")
|
|
78
|
+
|
|
79
|
+
yield Label("Description", classes="section-label")
|
|
80
|
+
yield Input(placeholder="Optional description", id="agent-description")
|
|
81
|
+
|
|
82
|
+
with Horizontal(id="buttons"):
|
|
83
|
+
yield Button("Cancel", id="cancel")
|
|
84
|
+
yield Button("Create", id="create", variant="primary")
|
|
85
|
+
|
|
86
|
+
def on_mount(self) -> None:
|
|
87
|
+
self.query_one("#agent-name", Input).focus()
|
|
88
|
+
|
|
89
|
+
def action_close(self) -> None:
|
|
90
|
+
self.dismiss(None)
|
|
91
|
+
|
|
92
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
93
|
+
button_id = event.button.id or ""
|
|
94
|
+
|
|
95
|
+
if button_id == "cancel":
|
|
96
|
+
self.dismiss(None)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if button_id == "create":
|
|
100
|
+
await self._create_agent()
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
104
|
+
"""Handle enter key in input fields."""
|
|
105
|
+
await self._create_agent()
|
|
106
|
+
|
|
107
|
+
async def _create_agent(self) -> None:
|
|
108
|
+
"""Create the agent profile."""
|
|
109
|
+
try:
|
|
110
|
+
from ...agent_names import generate_agent_name
|
|
111
|
+
from ...db import get_database
|
|
112
|
+
|
|
113
|
+
name_input = self.query_one("#agent-name", Input).value.strip()
|
|
114
|
+
description = self.query_one("#agent-description", Input).value.strip()
|
|
115
|
+
|
|
116
|
+
# Generate random name if not provided
|
|
117
|
+
name = name_input or generate_agent_name()
|
|
118
|
+
|
|
119
|
+
agent_id = str(uuid.uuid4())
|
|
120
|
+
|
|
121
|
+
db = get_database(read_only=False)
|
|
122
|
+
record = db.get_or_create_agent_info(agent_id, name=name)
|
|
123
|
+
if description:
|
|
124
|
+
record = db.update_agent_info(agent_id, description=description)
|
|
125
|
+
|
|
126
|
+
self.dismiss({
|
|
127
|
+
"id": record.id,
|
|
128
|
+
"name": record.name,
|
|
129
|
+
"description": record.description or "",
|
|
130
|
+
})
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Failed to create agent: {e}")
|
|
133
|
+
self.app.notify(f"Failed to create agent: {e}", severity="error")
|
|
@@ -6,9 +6,6 @@ import contextlib
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
import io
|
|
8
8
|
import json
|
|
9
|
-
import os
|
|
10
|
-
import shutil
|
|
11
|
-
import subprocess
|
|
12
9
|
from typing import Optional
|
|
13
10
|
|
|
14
11
|
from rich.markup import escape
|
|
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
|
|
|
20
17
|
from textual.widgets import DataTable, Static, TextArea
|
|
21
18
|
|
|
22
19
|
from ..widgets.openable_table import OpenableDataTable
|
|
20
|
+
from ..clipboard import copy_text
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
def _format_dt(dt: object) -> str:
|
|
@@ -246,36 +244,17 @@ class EventDetailScreen(ModalScreen):
|
|
|
246
244
|
pass
|
|
247
245
|
|
|
248
246
|
if exit_code == 0 and share_link:
|
|
247
|
+
from ..clipboard import copy_text
|
|
248
|
+
|
|
249
249
|
if not slack_message:
|
|
250
250
|
slack_message = getattr(self._event, "slack_message", None) if self._event else None
|
|
251
251
|
|
|
252
252
|
if slack_message:
|
|
253
|
-
|
|
253
|
+
text_to_copy = str(slack_message) + "\n\n" + str(share_link)
|
|
254
254
|
else:
|
|
255
|
-
|
|
255
|
+
text_to_copy = str(share_link)
|
|
256
256
|
|
|
257
|
-
copied =
|
|
258
|
-
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
259
|
-
try:
|
|
260
|
-
copied = (
|
|
261
|
-
subprocess.run(
|
|
262
|
-
["pbcopy"],
|
|
263
|
-
input=copy_text,
|
|
264
|
-
text=True,
|
|
265
|
-
capture_output=False,
|
|
266
|
-
check=False,
|
|
267
|
-
).returncode
|
|
268
|
-
== 0
|
|
269
|
-
)
|
|
270
|
-
except Exception:
|
|
271
|
-
copied = False
|
|
272
|
-
|
|
273
|
-
if not copied:
|
|
274
|
-
try:
|
|
275
|
-
self.app.copy_to_clipboard(copy_text)
|
|
276
|
-
copied = True
|
|
277
|
-
except Exception:
|
|
278
|
-
copied = False
|
|
257
|
+
copied = copy_text(self.app, text_to_copy)
|
|
279
258
|
|
|
280
259
|
suffix = " (copied)" if copied else ""
|
|
281
260
|
self.app.notify(f"Share link: {share_link}{suffix}", title="Share", timeout=6)
|
|
@@ -228,3 +228,70 @@ TerminalPanel .context-sessions Static {
|
|
|
228
228
|
text-align: left;
|
|
229
229
|
content-align: left middle;
|
|
230
230
|
}
|
|
231
|
+
|
|
232
|
+
/* Agents tab: compact layout matching terminal panel */
|
|
233
|
+
AgentsPanel {
|
|
234
|
+
padding: 0 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
AgentsPanel:focus {
|
|
238
|
+
border: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
AgentsPanel .summary {
|
|
242
|
+
background: transparent;
|
|
243
|
+
border: none;
|
|
244
|
+
padding: 0;
|
|
245
|
+
margin: 0 0 1 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
AgentsPanel .list {
|
|
249
|
+
background: transparent;
|
|
250
|
+
border: none;
|
|
251
|
+
padding: 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
AgentsPanel Button {
|
|
255
|
+
min-width: 0;
|
|
256
|
+
background: transparent;
|
|
257
|
+
border: none;
|
|
258
|
+
padding: 0 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
AgentsPanel Button:hover {
|
|
262
|
+
background: $surface-lighten-1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
AgentsPanel .agent-row Button.agent-name {
|
|
266
|
+
text-align: left;
|
|
267
|
+
content-align: left middle;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
AgentsPanel .agent-row Button.agent-create {
|
|
271
|
+
width: auto;
|
|
272
|
+
min-width: 8;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
AgentsPanel .agent-row Button.agent-delete {
|
|
276
|
+
padding: 0;
|
|
277
|
+
width: 3;
|
|
278
|
+
min-width: 3;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
AgentsPanel .terminal-list {
|
|
282
|
+
background: transparent;
|
|
283
|
+
border: none;
|
|
284
|
+
padding: 0;
|
|
285
|
+
margin: 0 0 1 2;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
AgentsPanel .terminal-row Button.terminal-switch {
|
|
289
|
+
text-align: left;
|
|
290
|
+
content-align: left top;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
AgentsPanel .terminal-row Button.terminal-close {
|
|
294
|
+
padding: 0;
|
|
295
|
+
width: 3;
|
|
296
|
+
min-width: 3;
|
|
297
|
+
}
|
|
@@ -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
|
|
|
@@ -9,6 +9,7 @@ from .config_panel import ConfigPanel
|
|
|
9
9
|
from .search_panel import SearchPanel
|
|
10
10
|
from .openable_table import OpenableDataTable
|
|
11
11
|
from .terminal_panel import TerminalPanel
|
|
12
|
+
from .agents_panel import AgentsPanel
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"AlineHeader",
|
|
@@ -20,4 +21,5 @@ __all__ = [
|
|
|
20
21
|
"SearchPanel",
|
|
21
22
|
"OpenableDataTable",
|
|
22
23
|
"TerminalPanel",
|
|
24
|
+
"AgentsPanel",
|
|
23
25
|
]
|