aline-ai 0.5.9__py3-none-any.whl → 0.5.11__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.9.dist-info → aline_ai-0.5.11.dist-info}/METADATA +1 -1
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.11.dist-info}/RECORD +17 -16
- realign/__init__.py +1 -1
- realign/claude_hooks/terminal_state.py +94 -3
- realign/context.py +140 -6
- realign/dashboard/screens/create_agent.py +69 -4
- realign/dashboard/tmux_manager.py +59 -2
- realign/dashboard/widgets/terminal_panel.py +14 -7
- realign/db/__init__.py +44 -0
- realign/db/base.py +36 -0
- realign/db/migrate_agents.py +303 -0
- realign/db/schema.py +177 -1
- realign/db/sqlite_db.py +617 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.11.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.11.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.11.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.11.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
aline_ai-0.5.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.5.11.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=J7JxBCOqQGzUmBxeTzF9YbnIPjgauOOl8ukALr-3pCM,1624
|
|
3
3
|
realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
|
|
4
4
|
realign/cli.py,sha256=9VS3WbysZ78NRK5EvkJVg8s6Uh2TQjsGX1E9Pl81pHc,31234
|
|
5
5
|
realign/codex_detector.py,sha256=N9ulgMgvTzDfXE4s4vLd6OoS0hT7R6h2bDFFXWa-2hE,4183
|
|
6
6
|
realign/config.py,sha256=lIKZqeOwYc_gHo760lYYX6PnapuKrCWGqT5SA8-PbeA,12044
|
|
7
|
-
realign/context.py,sha256=
|
|
7
|
+
realign/context.py,sha256=8hzgNOg-7_eMW22wt7OM5H9IsmMveKXCv0epG7E0G7w,13917
|
|
8
8
|
realign/file_lock.py,sha256=kLNm1Rra4TCrTMyPM5fwjVascq-CUz2Bzh9HHKtCKOE,3444
|
|
9
9
|
realign/hooks.py,sha256=NR4LgWgkA6npW_B68I7OdCaZNWseYSP7ZbK4Sl5nnTo,74692
|
|
10
10
|
realign/llm_client.py,sha256=KPfJScQvqse-Tm-VpqnZ6C5jvajPl2n4Ddz9sUp7WIY,24564
|
|
@@ -28,7 +28,7 @@ realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFr
|
|
|
28
28
|
realign/claude_hooks/permission_request_hook_installer.py,sha256=_8Wr_L5MES7iGukJzcaj4bqR0BH8kFL44U_X4iKtw2Y,7791
|
|
29
29
|
realign/claude_hooks/stop_hook.py,sha256=2nzF2aF1p5teMJ0eV0ALEHD1K-yVj5sSh7UE8xL54ZE,12025
|
|
30
30
|
realign/claude_hooks/stop_hook_installer.py,sha256=uyqKOqpix7CQP64ERBvvh7viSPp_wx_JVGNAX18rKh0,7228
|
|
31
|
-
realign/claude_hooks/terminal_state.py,sha256=
|
|
31
|
+
realign/claude_hooks/terminal_state.py,sha256=i8B6b_2_9ttPEemp7SrGdFRJSa-vm5lc7YSTRTvAWNg,5397
|
|
32
32
|
realign/claude_hooks/user_prompt_submit_hook.py,sha256=WD-UavhBTueN2TPfnZrnPC7DFYGEeptjUEF21EJn7Qo,10312
|
|
33
33
|
realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
|
|
34
34
|
realign/commands/__init__.py,sha256=sx_ck55oxaoiF4N3LugG0ZXwonUDxeEZ5uHbBKCC7K8,89
|
|
@@ -45,9 +45,9 @@ realign/commands/watcher.py,sha256=fWL3kaRkqE03-NtFLaXlx93hJAQrAuNPSoYhOyQZfq8,1
|
|
|
45
45
|
realign/commands/worker.py,sha256=K1DG1uZ--ebKwklHCyIFdN_axoLjL9Onx8Naq-DOZBs,23078
|
|
46
46
|
realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
|
|
47
47
|
realign/dashboard/app.py,sha256=jyW6mqmItTy253CPSqInxctkWzkrGEikdy-ikuShQ14,13299
|
|
48
|
-
realign/dashboard/tmux_manager.py,sha256=
|
|
48
|
+
realign/dashboard/tmux_manager.py,sha256=Vt_30WNtDg7c_9SEh8xdDtBLJ8kNq6bGSPh5r3VXpg0,26276
|
|
49
49
|
realign/dashboard/screens/__init__.py,sha256=US6sAmQs5VVkH2tFkH_z0WDT4H8cVhLL-JckfSR1yQY,446
|
|
50
|
-
realign/dashboard/screens/create_agent.py,sha256=
|
|
50
|
+
realign/dashboard/screens/create_agent.py,sha256=lpcT1zLq_p02codtHTE8KdbEzCEaNLnk1lqU3QLcXCg,10057
|
|
51
51
|
realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
|
|
52
52
|
realign/dashboard/screens/event_detail.py,sha256=OLaL3-FgAohDdzVlfuUw5yh2SR49IHIpCtiqXJhBTc0,20992
|
|
53
53
|
realign/dashboard/screens/help_screen.py,sha256=Icrcvbgyz49R2tBiu8vBZ4CLm6iYclv_-FTa2pCFRRQ,3398
|
|
@@ -61,15 +61,16 @@ realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMW
|
|
|
61
61
|
realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
|
|
62
62
|
realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
|
|
63
63
|
realign/dashboard/widgets/sessions_table.py,sha256=PohOkg-ESLBa-Sq0PdLPhV-YzVXOGpUo5ETs0MYO4u8,33415
|
|
64
|
-
realign/dashboard/widgets/terminal_panel.py,sha256=
|
|
64
|
+
realign/dashboard/widgets/terminal_panel.py,sha256=uoi3LjgYWyFE6Yr208KC5iKg0QxLcXpN6hCERlI6pBg,29069
|
|
65
65
|
realign/dashboard/widgets/watcher_panel.py,sha256=O_mdDacgc87xA-5KEfta53Ik_Xsk_B2OfwenMOTtGw8,19722
|
|
66
66
|
realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
|
|
67
|
-
realign/db/__init__.py,sha256
|
|
68
|
-
realign/db/base.py,sha256=
|
|
67
|
+
realign/db/__init__.py,sha256=65LsNdsq_rkwNC1eg1OAr3HC0ORXtelOh0I8MhNGr-g,3288
|
|
68
|
+
realign/db/base.py,sha256=MIqu08uG8i5atjZ9uF-uc0Rx35ondxCtUPK92hMoHx4,13179
|
|
69
69
|
realign/db/locks.py,sha256=yzCiPJZ4eOQX-Q4mXB6s76U2U7lXAzIBBy1t59w-AVU,1698
|
|
70
|
+
realign/db/migrate_agents.py,sha256=cDeVUzKW950dJ0lV74QObHuONqKwErSrXI5akU2vBmQ,9633
|
|
70
71
|
realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
|
|
71
|
-
realign/db/schema.py,sha256=
|
|
72
|
-
realign/db/sqlite_db.py,sha256=
|
|
72
|
+
realign/db/schema.py,sha256=93dfMtw3LgkBMpiUlCQ0EscY9RFsuS8sEBDckH8lGws,25864
|
|
73
|
+
realign/db/sqlite_db.py,sha256=sZXcvEaSu4C_MQ8pF20RUhwsPtBlNr6ANqf8suM5X8E,102660
|
|
73
74
|
realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
|
|
74
75
|
realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
|
|
75
76
|
realign/events/event_summarizer.py,sha256=ZLiwOXWN8eawep3cQs3Wh9QLSypvU1SRbe8GTJXJQaY,8272
|
|
@@ -88,8 +89,8 @@ realign/triggers/next_turn_trigger.py,sha256=BpP0PWn4mU1MZd6mv89jWcjs8Jtv0zEWapW
|
|
|
88
89
|
realign/triggers/registry.py,sha256=cb-AVLbYB2pqwfWL3q1DQxLv4kOw7g7m-GshTdfFESc,3827
|
|
89
90
|
realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
|
|
90
91
|
realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
|
|
91
|
-
aline_ai-0.5.
|
|
92
|
-
aline_ai-0.5.
|
|
93
|
-
aline_ai-0.5.
|
|
94
|
-
aline_ai-0.5.
|
|
95
|
-
aline_ai-0.5.
|
|
92
|
+
aline_ai-0.5.11.dist-info/METADATA,sha256=fq0cvqmYD9PExKiqsufTsOYkXU0Y90QcV4P0AyTQBMw,1598
|
|
93
|
+
aline_ai-0.5.11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
94
|
+
aline_ai-0.5.11.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
95
|
+
aline_ai-0.5.11.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
96
|
+
aline_ai-0.5.11.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ import json
|
|
|
11
11
|
import os
|
|
12
12
|
import time
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any, Optional
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _state_path() -> Path:
|
|
@@ -34,6 +34,74 @@ def _read_json(path: Path) -> dict[str, Any]:
|
|
|
34
34
|
return {}
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _get_db():
|
|
38
|
+
"""Get database connection (lazy import to avoid circular deps in hooks)."""
|
|
39
|
+
try:
|
|
40
|
+
from ..db import get_database
|
|
41
|
+
|
|
42
|
+
return get_database(read_only=False)
|
|
43
|
+
except Exception:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _write_to_db(
|
|
48
|
+
*,
|
|
49
|
+
terminal_id: str,
|
|
50
|
+
provider: str,
|
|
51
|
+
session_type: str,
|
|
52
|
+
session_id: str,
|
|
53
|
+
transcript_path: str = "",
|
|
54
|
+
cwd: str = "",
|
|
55
|
+
project_dir: str = "",
|
|
56
|
+
source: str = "",
|
|
57
|
+
context_id: Optional[str] = None,
|
|
58
|
+
attention: Optional[str] = None,
|
|
59
|
+
) -> bool:
|
|
60
|
+
"""Write terminal mapping to database (best-effort).
|
|
61
|
+
|
|
62
|
+
Returns True if successful, False otherwise.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
db = _get_db()
|
|
66
|
+
if not db:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Check if agent exists
|
|
70
|
+
existing = db.get_agent_by_id(terminal_id)
|
|
71
|
+
if existing:
|
|
72
|
+
# Update existing agent
|
|
73
|
+
db.update_agent(
|
|
74
|
+
terminal_id,
|
|
75
|
+
provider=provider,
|
|
76
|
+
session_type=session_type,
|
|
77
|
+
session_id=session_id if session_id else None,
|
|
78
|
+
transcript_path=transcript_path if transcript_path else None,
|
|
79
|
+
cwd=cwd if cwd else None,
|
|
80
|
+
project_dir=project_dir if project_dir else None,
|
|
81
|
+
source=source if source else None,
|
|
82
|
+
context_id=context_id,
|
|
83
|
+
attention=attention,
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
# Create new agent
|
|
87
|
+
db.get_or_create_agent(
|
|
88
|
+
terminal_id,
|
|
89
|
+
provider=provider,
|
|
90
|
+
session_type=session_type,
|
|
91
|
+
session_id=session_id if session_id else None,
|
|
92
|
+
context_id=context_id,
|
|
93
|
+
transcript_path=transcript_path if transcript_path else None,
|
|
94
|
+
cwd=cwd if cwd else None,
|
|
95
|
+
project_dir=project_dir if project_dir else None,
|
|
96
|
+
source=source if source else None,
|
|
97
|
+
attention=attention,
|
|
98
|
+
)
|
|
99
|
+
# Note: Don't close - get_database() returns a singleton
|
|
100
|
+
return True
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
37
105
|
def update_terminal_mapping(
|
|
38
106
|
*,
|
|
39
107
|
terminal_id: str,
|
|
@@ -44,11 +112,32 @@ def update_terminal_mapping(
|
|
|
44
112
|
cwd: str = "",
|
|
45
113
|
project_dir: str = "",
|
|
46
114
|
source: str = "",
|
|
115
|
+
context_id: Optional[str] = None,
|
|
116
|
+
attention: Optional[str] = None,
|
|
47
117
|
) -> None:
|
|
48
|
-
"""Update
|
|
118
|
+
"""Update terminal->session binding.
|
|
119
|
+
|
|
120
|
+
Writes to both:
|
|
121
|
+
1. SQLite database (primary storage, V15+)
|
|
122
|
+
2. ~/.aline/terminal.json (backward compatibility fallback)
|
|
49
123
|
|
|
50
|
-
Concurrency: uses a simple fcntl lock file; last writer wins, but updates are atomic.
|
|
124
|
+
Concurrency: uses a simple fcntl lock file for JSON; last writer wins, but updates are atomic.
|
|
51
125
|
"""
|
|
126
|
+
# Phase 1: Write to database (best-effort, don't fail if DB unavailable)
|
|
127
|
+
_write_to_db(
|
|
128
|
+
terminal_id=terminal_id,
|
|
129
|
+
provider=provider,
|
|
130
|
+
session_type=session_type,
|
|
131
|
+
session_id=session_id,
|
|
132
|
+
transcript_path=transcript_path,
|
|
133
|
+
cwd=cwd,
|
|
134
|
+
project_dir=project_dir,
|
|
135
|
+
source=source,
|
|
136
|
+
context_id=context_id,
|
|
137
|
+
attention=attention,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Phase 2: Write to JSON (backward compatibility)
|
|
52
141
|
state_path = _state_path()
|
|
53
142
|
lock_path = _lock_path()
|
|
54
143
|
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -74,6 +163,8 @@ def update_terminal_mapping(
|
|
|
74
163
|
"cwd": cwd,
|
|
75
164
|
"project_dir": project_dir,
|
|
76
165
|
"source": source,
|
|
166
|
+
"context_id": context_id,
|
|
167
|
+
"attention": attention,
|
|
77
168
|
"updated_at": time.time(),
|
|
78
169
|
}
|
|
79
170
|
|
realign/context.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Context management for limiting search scope.
|
|
2
2
|
|
|
3
|
-
This module manages
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This module manages context entries that can be used to limit search scope.
|
|
4
|
+
Data is stored in:
|
|
5
|
+
1. SQLite database (primary storage, V15+)
|
|
6
|
+
2. ~/.aline/load.json (backward compatibility fallback)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import json
|
|
@@ -19,6 +20,26 @@ LOAD_JSON_PATH = Path.home() / ".aline" / "load.json"
|
|
|
19
20
|
CONTEXT_ID_ENV_VAR = "ALINE_CONTEXT_ID"
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _get_db():
|
|
24
|
+
"""Get database connection (lazy import to avoid circular deps)."""
|
|
25
|
+
try:
|
|
26
|
+
from .db import get_database
|
|
27
|
+
|
|
28
|
+
return get_database(read_only=False)
|
|
29
|
+
except Exception:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_db_readonly():
|
|
34
|
+
"""Get read-only database connection."""
|
|
35
|
+
try:
|
|
36
|
+
from .db import get_database
|
|
37
|
+
|
|
38
|
+
return get_database(read_only=True)
|
|
39
|
+
except Exception:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
@dataclass
|
|
23
44
|
class ContextEntry:
|
|
24
45
|
"""A single context entry in load.json."""
|
|
@@ -78,15 +99,52 @@ class ContextConfig:
|
|
|
78
99
|
return cls(contexts=contexts, default=default)
|
|
79
100
|
|
|
80
101
|
|
|
102
|
+
def _load_context_config_from_db() -> Optional[ContextConfig]:
|
|
103
|
+
"""Load context configuration from database (best-effort)."""
|
|
104
|
+
try:
|
|
105
|
+
db = _get_db_readonly()
|
|
106
|
+
if not db:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
db_contexts = db.list_agent_contexts(limit=100)
|
|
110
|
+
if not db_contexts:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
contexts = []
|
|
114
|
+
for ctx in db_contexts:
|
|
115
|
+
entry = ContextEntry(
|
|
116
|
+
context_sessions=ctx.session_ids or [],
|
|
117
|
+
context_events=ctx.event_ids or [],
|
|
118
|
+
context_id=ctx.id,
|
|
119
|
+
workspace=ctx.workspace,
|
|
120
|
+
loaded_at=ctx.loaded_at,
|
|
121
|
+
)
|
|
122
|
+
contexts.append(entry)
|
|
123
|
+
|
|
124
|
+
return ContextConfig(contexts=contexts)
|
|
125
|
+
except Exception:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
81
129
|
def load_context_config(path: Optional[Path] = None) -> Optional[ContextConfig]:
|
|
82
|
-
"""Load context configuration
|
|
130
|
+
"""Load context configuration.
|
|
131
|
+
|
|
132
|
+
Priority:
|
|
133
|
+
1. SQLite database (primary storage, V15+)
|
|
134
|
+
2. ~/.aline/load.json (fallback for backward compatibility)
|
|
83
135
|
|
|
84
136
|
Args:
|
|
85
137
|
path: Optional path to load.json. Defaults to ~/.aline/load.json
|
|
86
138
|
|
|
87
139
|
Returns:
|
|
88
|
-
ContextConfig if
|
|
140
|
+
ContextConfig if data exists and is valid, None otherwise.
|
|
89
141
|
"""
|
|
142
|
+
# Phase 1: Try to load from database
|
|
143
|
+
db_config = _load_context_config_from_db()
|
|
144
|
+
if db_config and db_config.contexts:
|
|
145
|
+
return db_config
|
|
146
|
+
|
|
147
|
+
# Phase 2: Fall back to JSON file
|
|
90
148
|
config_path = path or LOAD_JSON_PATH
|
|
91
149
|
|
|
92
150
|
if not config_path.exists():
|
|
@@ -100,8 +158,55 @@ def load_context_config(path: Optional[Path] = None) -> Optional[ContextConfig]:
|
|
|
100
158
|
return None
|
|
101
159
|
|
|
102
160
|
|
|
161
|
+
def _sync_context_to_db(entry: ContextEntry) -> bool:
|
|
162
|
+
"""Sync a context entry to the database (best-effort)."""
|
|
163
|
+
if not entry.context_id:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
db = _get_db()
|
|
168
|
+
if not db:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
# Get or create the context
|
|
172
|
+
existing = db.get_agent_context_by_id(entry.context_id)
|
|
173
|
+
if existing:
|
|
174
|
+
db.update_agent_context(
|
|
175
|
+
entry.context_id,
|
|
176
|
+
workspace=entry.workspace,
|
|
177
|
+
loaded_at=entry.loaded_at,
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
db.get_or_create_agent_context(
|
|
181
|
+
entry.context_id,
|
|
182
|
+
workspace=entry.workspace,
|
|
183
|
+
loaded_at=entry.loaded_at,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Update session links
|
|
187
|
+
if entry.context_sessions:
|
|
188
|
+
db.set_agent_context_sessions(entry.context_id, entry.context_sessions)
|
|
189
|
+
else:
|
|
190
|
+
db.set_agent_context_sessions(entry.context_id, [])
|
|
191
|
+
|
|
192
|
+
# Update event links
|
|
193
|
+
if entry.context_events:
|
|
194
|
+
db.set_agent_context_events(entry.context_id, entry.context_events)
|
|
195
|
+
else:
|
|
196
|
+
db.set_agent_context_events(entry.context_id, [])
|
|
197
|
+
|
|
198
|
+
# Note: Don't close - get_database() returns a singleton
|
|
199
|
+
return True
|
|
200
|
+
except Exception:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
103
204
|
def save_context_config(config: ContextConfig, path: Optional[Path] = None) -> bool:
|
|
104
|
-
"""Save context configuration
|
|
205
|
+
"""Save context configuration.
|
|
206
|
+
|
|
207
|
+
Writes to both:
|
|
208
|
+
1. SQLite database (primary storage, V15+)
|
|
209
|
+
2. ~/.aline/load.json (backward compatibility fallback)
|
|
105
210
|
|
|
106
211
|
Args:
|
|
107
212
|
config: ContextConfig to save
|
|
@@ -110,6 +215,13 @@ def save_context_config(config: ContextConfig, path: Optional[Path] = None) -> b
|
|
|
110
215
|
Returns:
|
|
111
216
|
True if successful, False otherwise.
|
|
112
217
|
"""
|
|
218
|
+
# Phase 1: Sync all contexts to database (best-effort)
|
|
219
|
+
for entry in config.contexts:
|
|
220
|
+
_sync_context_to_db(entry)
|
|
221
|
+
if config.default and config.default.context_id:
|
|
222
|
+
_sync_context_to_db(config.default)
|
|
223
|
+
|
|
224
|
+
# Phase 2: Write to JSON (backward compatibility)
|
|
113
225
|
config_path = path or LOAD_JSON_PATH
|
|
114
226
|
|
|
115
227
|
# Ensure parent directory exists
|
|
@@ -317,6 +429,11 @@ def get_context_by_id(
|
|
|
317
429
|
) -> Optional[ContextEntry]:
|
|
318
430
|
"""Get a specific context by its ID.
|
|
319
431
|
|
|
432
|
+
Priority:
|
|
433
|
+
1. SQLite database (primary storage, V15+)
|
|
434
|
+
2. In-memory config if provided
|
|
435
|
+
3. ~/.aline/load.json (fallback)
|
|
436
|
+
|
|
320
437
|
Args:
|
|
321
438
|
context_id: The context ID to look up
|
|
322
439
|
config: Existing config to search
|
|
@@ -325,6 +442,23 @@ def get_context_by_id(
|
|
|
325
442
|
Returns:
|
|
326
443
|
The ContextEntry if found, None otherwise.
|
|
327
444
|
"""
|
|
445
|
+
# Phase 1: Try to load from database
|
|
446
|
+
try:
|
|
447
|
+
db = _get_db_readonly()
|
|
448
|
+
if db:
|
|
449
|
+
ctx = db.get_agent_context_by_id(context_id)
|
|
450
|
+
if ctx:
|
|
451
|
+
return ContextEntry(
|
|
452
|
+
context_sessions=ctx.session_ids or [],
|
|
453
|
+
context_events=ctx.event_ids or [],
|
|
454
|
+
context_id=ctx.id,
|
|
455
|
+
workspace=ctx.workspace,
|
|
456
|
+
loaded_at=ctx.loaded_at,
|
|
457
|
+
)
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
# Phase 2: Check in-memory config or load from JSON
|
|
328
462
|
if config is None:
|
|
329
463
|
config = load_context_config(path)
|
|
330
464
|
|
|
@@ -40,23 +40,45 @@ def _load_last_workspace() -> str:
|
|
|
40
40
|
|
|
41
41
|
def _save_last_workspace(path: str) -> None:
|
|
42
42
|
"""Save the last used workspace path to state file."""
|
|
43
|
+
_save_state("last_workspace", path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_claude_permission_mode() -> str:
|
|
47
|
+
"""Load the last used Claude permission mode from state file."""
|
|
48
|
+
try:
|
|
49
|
+
if DASHBOARD_STATE_FILE.exists():
|
|
50
|
+
with open(DASHBOARD_STATE_FILE, "r", encoding="utf-8") as f:
|
|
51
|
+
state = json.load(f)
|
|
52
|
+
return state.get("claude_permission_mode", "normal")
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
return "normal"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _save_claude_permission_mode(mode: str) -> None:
|
|
59
|
+
"""Save the Claude permission mode to state file."""
|
|
60
|
+
_save_state("claude_permission_mode", mode)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _save_state(key: str, value: str) -> None:
|
|
64
|
+
"""Save a key-value pair to the state file."""
|
|
43
65
|
try:
|
|
44
66
|
DASHBOARD_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
45
67
|
state = {}
|
|
46
68
|
if DASHBOARD_STATE_FILE.exists():
|
|
47
69
|
with open(DASHBOARD_STATE_FILE, "r", encoding="utf-8") as f:
|
|
48
70
|
state = json.load(f)
|
|
49
|
-
state[
|
|
71
|
+
state[key] = value
|
|
50
72
|
with open(DASHBOARD_STATE_FILE, "w", encoding="utf-8") as f:
|
|
51
73
|
json.dump(state, f, indent=2)
|
|
52
74
|
except Exception:
|
|
53
75
|
pass
|
|
54
76
|
|
|
55
77
|
|
|
56
|
-
class CreateAgentScreen(ModalScreen[Optional[tuple[str, str]]]):
|
|
78
|
+
class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
|
|
57
79
|
"""Modal to create a new agent terminal.
|
|
58
80
|
|
|
59
|
-
Returns a tuple of (agent_type, workspace_path) on success, None on cancel.
|
|
81
|
+
Returns a tuple of (agent_type, workspace_path, skip_permissions) on success, None on cancel.
|
|
60
82
|
"""
|
|
61
83
|
|
|
62
84
|
BINDINGS = [
|
|
@@ -110,6 +132,15 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str]]]):
|
|
|
110
132
|
margin-top: 1;
|
|
111
133
|
}
|
|
112
134
|
|
|
135
|
+
CreateAgentScreen #claude-options {
|
|
136
|
+
height: auto;
|
|
137
|
+
margin-top: 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
CreateAgentScreen #claude-options.hidden {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
113
144
|
CreateAgentScreen #workspace-row {
|
|
114
145
|
height: auto;
|
|
115
146
|
margin-top: 0;
|
|
@@ -143,6 +174,7 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str]]]):
|
|
|
143
174
|
def __init__(self) -> None:
|
|
144
175
|
super().__init__()
|
|
145
176
|
self._workspace_path = _load_last_workspace()
|
|
177
|
+
self._permission_mode = _load_claude_permission_mode()
|
|
146
178
|
|
|
147
179
|
def compose(self) -> ComposeResult:
|
|
148
180
|
with Container(id="create-agent-root"):
|
|
@@ -161,16 +193,39 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str]]]):
|
|
|
161
193
|
yield Static(self._workspace_path, id="workspace-path")
|
|
162
194
|
yield Button("Browse", id="browse-btn", variant="default")
|
|
163
195
|
|
|
196
|
+
with Vertical(id="claude-options"):
|
|
197
|
+
yield Label("Permission Mode", classes="section-label")
|
|
198
|
+
with RadioSet(id="permission-mode"):
|
|
199
|
+
yield RadioButton("Normal", id="perm-normal", value=True)
|
|
200
|
+
yield RadioButton("Skip (--dangerously-skip-permissions)", id="perm-skip")
|
|
201
|
+
|
|
164
202
|
with Horizontal(id="buttons"):
|
|
165
203
|
yield Button("Cancel", id="cancel")
|
|
166
204
|
yield Button("Create", id="create", variant="primary")
|
|
167
205
|
|
|
168
206
|
def on_mount(self) -> None:
|
|
207
|
+
# Set the saved permission mode
|
|
208
|
+
if self._permission_mode == "skip":
|
|
209
|
+
self.query_one("#perm-skip", RadioButton).value = True
|
|
210
|
+
else:
|
|
211
|
+
self.query_one("#perm-normal", RadioButton).value = True
|
|
169
212
|
self.query_one("#create", Button).focus()
|
|
170
213
|
|
|
171
214
|
def action_close(self) -> None:
|
|
172
215
|
self.dismiss(None)
|
|
173
216
|
|
|
217
|
+
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
|
|
218
|
+
"""Show/hide Claude options based on selected agent type."""
|
|
219
|
+
# Only handle agent type changes
|
|
220
|
+
if event.radio_set.id != "agent-type":
|
|
221
|
+
return
|
|
222
|
+
claude_options = self.query_one("#claude-options", Vertical)
|
|
223
|
+
is_claude = event.pressed.id == "type-claude" if event.pressed else False
|
|
224
|
+
if is_claude:
|
|
225
|
+
claude_options.remove_class("hidden")
|
|
226
|
+
else:
|
|
227
|
+
claude_options.add_class("hidden")
|
|
228
|
+
|
|
174
229
|
def _update_workspace_display(self) -> None:
|
|
175
230
|
"""Update the workspace path display."""
|
|
176
231
|
self.query_one("#workspace-path", Static).update(self._workspace_path)
|
|
@@ -237,8 +292,18 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str]]]):
|
|
|
237
292
|
}
|
|
238
293
|
agent_type = agent_type_map.get(pressed_button.id or "", "claude")
|
|
239
294
|
|
|
295
|
+
# Get permission mode (only relevant for Claude)
|
|
296
|
+
skip_permissions = False
|
|
297
|
+
if agent_type == "claude":
|
|
298
|
+
perm_radio_set = self.query_one("#permission-mode", RadioSet)
|
|
299
|
+
perm_pressed = perm_radio_set.pressed_button
|
|
300
|
+
skip_permissions = perm_pressed is not None and perm_pressed.id == "perm-skip"
|
|
301
|
+
# Save the permission mode for next time
|
|
302
|
+
permission_mode = "skip" if skip_permissions else "normal"
|
|
303
|
+
_save_claude_permission_mode(permission_mode)
|
|
304
|
+
|
|
240
305
|
# Save the workspace path for next time
|
|
241
306
|
_save_last_workspace(self._workspace_path)
|
|
242
307
|
|
|
243
308
|
# Return the result
|
|
244
|
-
self.dismiss((agent_type, self._workspace_path))
|
|
309
|
+
self.dismiss((agent_type, self._workspace_path, skip_permissions))
|
|
@@ -191,8 +191,43 @@ def _session_id_from_transcript_path(transcript_path: str | None) -> str | None:
|
|
|
191
191
|
return stem
|
|
192
192
|
|
|
193
193
|
|
|
194
|
-
def
|
|
195
|
-
"""Load
|
|
194
|
+
def _load_terminal_state_from_db() -> dict[str, dict[str, str]]:
|
|
195
|
+
"""Load terminal state from database (best-effort)."""
|
|
196
|
+
try:
|
|
197
|
+
from ..db import get_database
|
|
198
|
+
|
|
199
|
+
db = get_database(read_only=True)
|
|
200
|
+
agents = db.list_agents(status="active", limit=100)
|
|
201
|
+
|
|
202
|
+
out: dict[str, dict[str, str]] = {}
|
|
203
|
+
for agent in agents:
|
|
204
|
+
data: dict[str, str] = {}
|
|
205
|
+
if agent.provider:
|
|
206
|
+
data["provider"] = agent.provider
|
|
207
|
+
if agent.session_type:
|
|
208
|
+
data["session_type"] = agent.session_type
|
|
209
|
+
if agent.session_id:
|
|
210
|
+
data["session_id"] = agent.session_id
|
|
211
|
+
if agent.transcript_path:
|
|
212
|
+
data["transcript_path"] = agent.transcript_path
|
|
213
|
+
if agent.cwd:
|
|
214
|
+
data["cwd"] = agent.cwd
|
|
215
|
+
if agent.project_dir:
|
|
216
|
+
data["project_dir"] = agent.project_dir
|
|
217
|
+
if agent.source:
|
|
218
|
+
data["source"] = agent.source
|
|
219
|
+
if agent.context_id:
|
|
220
|
+
data["context_id"] = agent.context_id
|
|
221
|
+
if agent.attention:
|
|
222
|
+
data["attention"] = agent.attention
|
|
223
|
+
out[agent.id] = data
|
|
224
|
+
return out
|
|
225
|
+
except Exception:
|
|
226
|
+
return {}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _load_terminal_state_from_json() -> dict[str, dict[str, str]]:
|
|
230
|
+
"""Load terminal state from JSON file (fallback)."""
|
|
196
231
|
try:
|
|
197
232
|
path = Path.home() / ".aline" / "terminal.json"
|
|
198
233
|
if not path.exists():
|
|
@@ -212,6 +247,28 @@ def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
|
212
247
|
return {}
|
|
213
248
|
|
|
214
249
|
|
|
250
|
+
def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
251
|
+
"""Load terminal state.
|
|
252
|
+
|
|
253
|
+
Priority:
|
|
254
|
+
1. SQLite database (primary storage, V15+)
|
|
255
|
+
2. ~/.aline/terminal.json (fallback for backward compatibility)
|
|
256
|
+
|
|
257
|
+
Merges both sources, with DB taking precedence.
|
|
258
|
+
"""
|
|
259
|
+
# Phase 1: Load from database
|
|
260
|
+
db_state = _load_terminal_state_from_db()
|
|
261
|
+
|
|
262
|
+
# Phase 2: Load from JSON as fallback
|
|
263
|
+
json_state = _load_terminal_state_from_json()
|
|
264
|
+
|
|
265
|
+
# Merge: DB takes precedence, JSON provides fallback for entries not in DB
|
|
266
|
+
result = dict(json_state)
|
|
267
|
+
result.update(db_state)
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
|
|
215
272
|
def _aline_tmux_conf_path() -> Path:
|
|
216
273
|
return Path.home() / ".aline" / "tmux" / "tmux.conf"
|
|
217
274
|
|
|
@@ -623,22 +623,24 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
623
623
|
"""Wrap a command to run in a specific directory."""
|
|
624
624
|
return f"cd {shlex.quote(directory)} && {command}"
|
|
625
625
|
|
|
626
|
-
def _on_create_agent_result(self, result: tuple[str, str] | None) -> None:
|
|
626
|
+
def _on_create_agent_result(self, result: tuple[str, str, bool] | None) -> None:
|
|
627
627
|
"""Handle the result from CreateAgentScreen modal."""
|
|
628
628
|
if result is None:
|
|
629
629
|
return
|
|
630
630
|
|
|
631
|
-
agent_type, workspace = result
|
|
631
|
+
agent_type, workspace, skip_permissions = result
|
|
632
632
|
self.run_worker(
|
|
633
|
-
self._create_agent(agent_type, workspace),
|
|
633
|
+
self._create_agent(agent_type, workspace, skip_permissions=skip_permissions),
|
|
634
634
|
group="terminal-panel-create",
|
|
635
635
|
exclusive=True,
|
|
636
636
|
)
|
|
637
637
|
|
|
638
|
-
async def _create_agent(
|
|
638
|
+
async def _create_agent(
|
|
639
|
+
self, agent_type: str, workspace: str, *, skip_permissions: bool = False
|
|
640
|
+
) -> None:
|
|
639
641
|
"""Create a new agent terminal based on the selected type and workspace."""
|
|
640
642
|
if agent_type == "claude":
|
|
641
|
-
await self._create_claude_terminal(workspace)
|
|
643
|
+
await self._create_claude_terminal(workspace, skip_permissions=skip_permissions)
|
|
642
644
|
elif agent_type == "codex":
|
|
643
645
|
await self._create_codex_terminal(workspace)
|
|
644
646
|
elif agent_type == "opencode":
|
|
@@ -647,7 +649,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
647
649
|
await self._create_zsh_terminal(workspace)
|
|
648
650
|
await self.refresh_data()
|
|
649
651
|
|
|
650
|
-
async def _create_claude_terminal(
|
|
652
|
+
async def _create_claude_terminal(
|
|
653
|
+
self, workspace: str, *, skip_permissions: bool = False
|
|
654
|
+
) -> None:
|
|
651
655
|
"""Create a new Claude terminal."""
|
|
652
656
|
terminal_id = tmux_manager.new_terminal_id()
|
|
653
657
|
context_id = tmux_manager.new_context_id("cc")
|
|
@@ -708,8 +712,11 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
708
712
|
except Exception:
|
|
709
713
|
pass
|
|
710
714
|
|
|
715
|
+
claude_cmd = "claude"
|
|
716
|
+
if skip_permissions:
|
|
717
|
+
claude_cmd = "claude --dangerously-skip-permissions"
|
|
711
718
|
command = self._command_in_directory(
|
|
712
|
-
tmux_manager.zsh_run_and_keep_open(
|
|
719
|
+
tmux_manager.zsh_run_and_keep_open(claude_cmd), workspace
|
|
713
720
|
)
|
|
714
721
|
created = tmux_manager.create_inner_window(
|
|
715
722
|
"cc",
|