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
realign/commands/worker.py
CHANGED
|
@@ -87,7 +87,11 @@ def worker_status_command(verbose: bool = False, *, json_output: bool = False) -
|
|
|
87
87
|
# Show job counts if DB available
|
|
88
88
|
try:
|
|
89
89
|
dbi = get_database()
|
|
90
|
-
db_path =
|
|
90
|
+
db_path = (
|
|
91
|
+
Path(str(getattr(dbi, "db_path", ""))).expanduser()
|
|
92
|
+
if isinstance(dbi, SQLiteDatabase)
|
|
93
|
+
else None
|
|
94
|
+
)
|
|
91
95
|
if isinstance(dbi, SQLiteDatabase):
|
|
92
96
|
counts = dbi.get_job_counts()
|
|
93
97
|
turn_jobs = dbi.list_jobs(limit=30, kinds=["turn_summary"])
|
|
@@ -116,6 +120,7 @@ def worker_status_command(verbose: bool = False, *, json_output: bool = False) -
|
|
|
116
120
|
job_kinds = {}
|
|
117
121
|
|
|
118
122
|
if json_output:
|
|
123
|
+
|
|
119
124
|
def _session_id_from_job(job: dict) -> str:
|
|
120
125
|
payload = job.get("payload") or {}
|
|
121
126
|
session_id = str(payload.get("session_id") or "").strip()
|
|
@@ -133,7 +138,11 @@ def worker_status_command(verbose: bool = False, *, json_output: bool = False) -
|
|
|
133
138
|
session_type_by_id: dict[str, str] = {}
|
|
134
139
|
if isinstance(dbi, SQLiteDatabase):
|
|
135
140
|
session_ids = sorted(
|
|
136
|
-
{
|
|
141
|
+
{
|
|
142
|
+
sid
|
|
143
|
+
for sid in (_session_id_from_job(j) for j in (turn_jobs + session_jobs))
|
|
144
|
+
if sid
|
|
145
|
+
}
|
|
137
146
|
)
|
|
138
147
|
if session_ids:
|
|
139
148
|
for s in dbi.get_sessions_by_ids(session_ids):
|
|
@@ -196,7 +205,12 @@ def worker_status_command(verbose: bool = False, *, json_output: bool = False) -
|
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
data = {
|
|
199
|
-
"worker": {
|
|
208
|
+
"worker": {
|
|
209
|
+
"status": status.lower(),
|
|
210
|
+
"running": is_running,
|
|
211
|
+
"pid": pid,
|
|
212
|
+
"mode": mode,
|
|
213
|
+
},
|
|
200
214
|
"db_path": str(db_path) if db_path else None,
|
|
201
215
|
"job_counts": counts,
|
|
202
216
|
"job_kinds": job_kinds,
|
|
@@ -206,7 +220,9 @@ def worker_status_command(verbose: bool = False, *, json_output: bool = False) -
|
|
|
206
220
|
print(json.dumps(data, ensure_ascii=True))
|
|
207
221
|
return 0
|
|
208
222
|
|
|
209
|
-
console.print(
|
|
223
|
+
console.print(
|
|
224
|
+
f"[bold]Worker:[/bold] {status}" + (f" (PID: {pid}, mode: {mode})" if pid else "")
|
|
225
|
+
)
|
|
210
226
|
if db_path:
|
|
211
227
|
console.print(f"[dim]DB:[/dim] {db_path}")
|
|
212
228
|
if counts:
|
|
@@ -225,6 +241,7 @@ def worker_status_command(verbose: bool = False, *, json_output: bool = False) -
|
|
|
225
241
|
console.print(f"[dim]Job kinds:[/dim] {pairs}")
|
|
226
242
|
|
|
227
243
|
try:
|
|
244
|
+
|
|
228
245
|
def fmt_status(s: str) -> tuple[str, str]:
|
|
229
246
|
s = (s or "").lower()
|
|
230
247
|
if s == "processing":
|
|
@@ -254,7 +271,11 @@ def worker_status_command(verbose: bool = False, *, json_output: bool = False) -
|
|
|
254
271
|
session_type_by_id: dict[str, str] = {}
|
|
255
272
|
if isinstance(dbi, SQLiteDatabase):
|
|
256
273
|
session_ids = sorted(
|
|
257
|
-
{
|
|
274
|
+
{
|
|
275
|
+
sid
|
|
276
|
+
for sid in (_session_id_from_job(j) for j in (turn_jobs + session_jobs))
|
|
277
|
+
if sid
|
|
278
|
+
}
|
|
258
279
|
)
|
|
259
280
|
if session_ids:
|
|
260
281
|
for s in dbi.get_sessions_by_ids(session_ids):
|
|
@@ -501,7 +522,9 @@ def worker_stop_command() -> int:
|
|
|
501
522
|
pid, mode = all_processes[0]
|
|
502
523
|
console.print(f"[cyan]Stopping worker (PID: {pid}, mode: {mode})...[/cyan]")
|
|
503
524
|
else:
|
|
504
|
-
console.print(
|
|
525
|
+
console.print(
|
|
526
|
+
f"[cyan]Found {len(all_processes)} worker processes, stopping all...[/cyan]"
|
|
527
|
+
)
|
|
505
528
|
|
|
506
529
|
for pid, _mode in all_processes:
|
|
507
530
|
try:
|
realign/config.py
CHANGED
|
@@ -12,36 +12,38 @@ class ReAlignConfig:
|
|
|
12
12
|
"""ReAlign configuration."""
|
|
13
13
|
|
|
14
14
|
summary_max_chars: int = 500
|
|
15
|
-
redact_on_match: bool = False
|
|
15
|
+
redact_on_match: bool = False # Default: disable redaction (can be enabled in config)
|
|
16
16
|
hooks_installation: str = "repo"
|
|
17
17
|
sqlite_db_path: str = "~/.aline/db/aline.db" # Path to SQLite database
|
|
18
18
|
use_LLM: bool = True
|
|
19
|
-
llm_provider: str = "auto"
|
|
19
|
+
llm_provider: str = "auto" # LLM provider: "auto", "claude", or "openai"
|
|
20
20
|
auto_detect_claude: bool = True # Enable Claude Code session auto-detection
|
|
21
|
-
auto_detect_codex: bool = True
|
|
22
|
-
auto_detect_gemini: bool = True
|
|
23
|
-
auto_detect_antigravity: bool = False
|
|
24
|
-
mcp_auto_commit: bool = True
|
|
21
|
+
auto_detect_codex: bool = True # Enable Codex session auto-detection
|
|
22
|
+
auto_detect_gemini: bool = True # Enable Gemini CLI session auto-detection
|
|
23
|
+
auto_detect_antigravity: bool = False # Enable Antigravity IDE brain artifact monitoring
|
|
24
|
+
mcp_auto_commit: bool = True # Enable watcher auto-commit after each user request completes
|
|
25
25
|
enable_temp_turn_titles: bool = True # Generate temporary turn titles on user prompt submit
|
|
26
|
-
share_backend_url: str =
|
|
26
|
+
share_backend_url: str = (
|
|
27
|
+
"https://realign-server.vercel.app" # Backend URL for interactive share export
|
|
28
|
+
)
|
|
27
29
|
|
|
28
30
|
# User identity (V9)
|
|
29
|
-
user_name: str = ""
|
|
30
|
-
user_id: str = ""
|
|
31
|
+
user_name: str = "" # User's display name (set during init)
|
|
32
|
+
user_id: str = "" # User's UUID (generated from MAC address)
|
|
31
33
|
|
|
32
34
|
# Session catch-up settings
|
|
33
|
-
max_catchup_sessions: int = 3
|
|
35
|
+
max_catchup_sessions: int = 3 # Max sessions to auto-import on watcher startup
|
|
34
36
|
|
|
35
37
|
# LLM API Keys
|
|
36
|
-
anthropic_api_key: Optional[str] = None
|
|
37
|
-
openai_api_key: Optional[str] = None
|
|
38
|
+
anthropic_api_key: Optional[str] = None # Anthropic API key (set in config, not environment)
|
|
39
|
+
openai_api_key: Optional[str] = None # OpenAI API key (set in config, not environment)
|
|
38
40
|
|
|
39
41
|
# LLM Model Configuration
|
|
40
42
|
llm_anthropic_model: str = "claude-3-5-haiku-20241022" # Claude model to use
|
|
41
|
-
llm_openai_model: str = "gpt-4o-mini"
|
|
42
|
-
llm_openai_use_responses: bool = False
|
|
43
|
-
llm_max_tokens: int = 1000
|
|
44
|
-
llm_temperature: float = 0.0
|
|
43
|
+
llm_openai_model: str = "gpt-4o-mini" # OpenAI model to use
|
|
44
|
+
llm_openai_use_responses: bool = False # Use OpenAI Responses API for reasoning models
|
|
45
|
+
llm_max_tokens: int = 1000 # Default max tokens
|
|
46
|
+
llm_temperature: float = 0.0 # Default temperature (0.0 = deterministic)
|
|
45
47
|
|
|
46
48
|
@classmethod
|
|
47
49
|
def load(cls, config_path: Optional[Path] = None) -> "ReAlignConfig":
|
|
@@ -49,12 +51,13 @@ class ReAlignConfig:
|
|
|
49
51
|
if config_path is None:
|
|
50
52
|
# Default to new location: ~/.aline/config.yaml
|
|
51
53
|
config_path = Path.home() / ".aline" / "config.yaml"
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
# Check for legacy config and migrate if needed
|
|
54
56
|
legacy_path = Path.home() / ".config" / "realign" / "config.yaml"
|
|
55
57
|
if not config_path.exists() and legacy_path.exists():
|
|
56
58
|
try:
|
|
57
59
|
import shutil
|
|
60
|
+
|
|
58
61
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
62
|
shutil.move(legacy_path, config_path)
|
|
60
63
|
# Try to remove empty legacy directory
|
|
@@ -172,10 +175,11 @@ def generate_user_id() -> str:
|
|
|
172
175
|
str: User UUID as a string
|
|
173
176
|
"""
|
|
174
177
|
import uuid
|
|
178
|
+
|
|
175
179
|
try:
|
|
176
180
|
mac = uuid.getnode()
|
|
177
181
|
# Use MAC address with DNS namespace to generate UUID5
|
|
178
|
-
namespace = uuid.UUID(
|
|
182
|
+
namespace = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8") # DNS namespace
|
|
179
183
|
user_uuid = uuid.uuid5(namespace, str(mac))
|
|
180
184
|
return str(user_uuid)
|
|
181
185
|
except Exception:
|
|
@@ -194,8 +198,9 @@ def generate_random_username() -> str:
|
|
|
194
198
|
"""
|
|
195
199
|
import random
|
|
196
200
|
import string
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
|
|
202
|
+
letters = "".join(random.choices(string.ascii_lowercase, k=3))
|
|
203
|
+
digits = "".join(random.choices(string.digits, k=3))
|
|
199
204
|
return letters + digits
|
|
200
205
|
|
|
201
206
|
|
realign/context.py
CHANGED
|
@@ -245,9 +245,7 @@ def add_context(
|
|
|
245
245
|
entry = config.contexts[existing_idx]
|
|
246
246
|
# Add new sessions/events (merge, deduplicate)
|
|
247
247
|
if sessions:
|
|
248
|
-
entry.context_sessions = list(
|
|
249
|
-
set(entry.context_sessions) | set(sessions)
|
|
250
|
-
)
|
|
248
|
+
entry.context_sessions = list(set(entry.context_sessions) | set(sessions))
|
|
251
249
|
if events:
|
|
252
250
|
entry.context_events = list(set(entry.context_events) | set(events))
|
|
253
251
|
entry.loaded_at = datetime.utcnow().isoformat() + "Z"
|
realign/dashboard/app.py
CHANGED
|
@@ -54,44 +54,56 @@ 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
|
-
with TabbedContent(initial=tab_ids[0] if tab_ids else "
|
|
77
|
-
with TabPane("
|
|
78
|
-
yield
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
with TabbedContent(initial=tab_ids[0] if tab_ids else "terminal"):
|
|
86
|
+
with TabPane("Agents", id="terminal"):
|
|
87
|
+
yield TerminalPanel()
|
|
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"):
|
|
82
94
|
yield SessionsTable()
|
|
83
|
-
with TabPane("
|
|
95
|
+
with TabPane("Share", id="events"):
|
|
84
96
|
yield EventsTable()
|
|
85
97
|
with TabPane("Config", id="config"):
|
|
86
98
|
yield ConfigPanel()
|
|
87
99
|
with TabPane("Search", id="search"):
|
|
88
100
|
yield SearchPanel()
|
|
89
|
-
with TabPane("Terminal", id="terminal"):
|
|
90
|
-
yield TerminalPanel()
|
|
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))
|
|
@@ -121,7 +121,9 @@ class CreateEventScreen(ModalScreen):
|
|
|
121
121
|
|
|
122
122
|
self.query_one("#create", Button).disabled = True
|
|
123
123
|
self.query_one("#cancel", Button).disabled = True
|
|
124
|
-
self.app.notify(
|
|
124
|
+
self.app.notify(
|
|
125
|
+
"Generating event (aline watcher event generate)...", title="Create Event", timeout=2
|
|
126
|
+
)
|
|
125
127
|
|
|
126
128
|
self._worker = self.run_worker(work, thread=True, exit_on_error=False)
|
|
127
129
|
|
|
@@ -284,7 +284,9 @@ class EventDetailScreen(ModalScreen):
|
|
|
284
284
|
else:
|
|
285
285
|
extra = result.get("stderr") or ""
|
|
286
286
|
suffix = f": {extra}" if extra else ""
|
|
287
|
-
self.app.notify(
|
|
287
|
+
self.app.notify(
|
|
288
|
+
f"Share export failed (exit {exit_code}){suffix}", title="Share", timeout=6
|
|
289
|
+
)
|
|
288
290
|
|
|
289
291
|
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
290
292
|
if event.data_table.id != "event-sessions-table":
|
|
@@ -365,7 +367,7 @@ class EventDetailScreen(ModalScreen):
|
|
|
365
367
|
"antigravity": "Antigravity",
|
|
366
368
|
}
|
|
367
369
|
source = source_map.get(session_type, session_type)
|
|
368
|
-
project =
|
|
370
|
+
project = workspace.split("/")[-1] if workspace else "-"
|
|
369
371
|
|
|
370
372
|
self._sessions.append(
|
|
371
373
|
{
|
|
@@ -374,7 +376,9 @@ class EventDetailScreen(ModalScreen):
|
|
|
374
376
|
"source": source,
|
|
375
377
|
"project": project,
|
|
376
378
|
"turns": int(turn_count or 0),
|
|
377
|
-
"total_turns":
|
|
379
|
+
"total_turns": (
|
|
380
|
+
int(total_turns or 0) if total_turns is not None else None
|
|
381
|
+
),
|
|
378
382
|
"title": title,
|
|
379
383
|
"session_summary": session_summary,
|
|
380
384
|
"last_activity": last_activity,
|
|
@@ -397,7 +401,7 @@ class EventDetailScreen(ModalScreen):
|
|
|
397
401
|
"antigravity": "Antigravity",
|
|
398
402
|
}
|
|
399
403
|
source = source_map.get(session_type, session_type)
|
|
400
|
-
project =
|
|
404
|
+
project = str(workspace).split("/")[-1] if workspace else "-"
|
|
401
405
|
|
|
402
406
|
self._sessions.append(
|
|
403
407
|
{
|
|
@@ -467,7 +471,9 @@ class EventDetailScreen(ModalScreen):
|
|
|
467
471
|
selected_session_id: Optional[str] = None
|
|
468
472
|
try:
|
|
469
473
|
if table.row_count > 0:
|
|
470
|
-
selected_session_id = str(
|
|
474
|
+
selected_session_id = str(
|
|
475
|
+
table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
|
|
476
|
+
)
|
|
471
477
|
except Exception:
|
|
472
478
|
selected_session_id = None
|
|
473
479
|
table.clear()
|
|
@@ -529,7 +535,9 @@ class EventDetailScreen(ModalScreen):
|
|
|
529
535
|
"gemini": "Gemini",
|
|
530
536
|
"antigravity": "Antigravity",
|
|
531
537
|
}
|
|
532
|
-
source = source_map.get(
|
|
538
|
+
source = source_map.get(
|
|
539
|
+
record_type or "", record_type or session.get("source") or "unknown"
|
|
540
|
+
)
|
|
533
541
|
|
|
534
542
|
title = record_title or session.get("title") or "(no title)"
|
|
535
543
|
summary = record_summary or session.get("session_summary") or ""
|