aline-ai 0.5.9__py3-none-any.whl → 0.5.10__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.10.dist-info}/METADATA +1 -1
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/RECORD +17 -16
- realign/__init__.py +1 -1
- realign/claude_hooks/terminal_state.py +86 -3
- realign/context.py +136 -6
- realign/dashboard/screens/create_agent.py +69 -4
- realign/dashboard/tmux_manager.py +62 -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 +297 -0
- realign/db/schema.py +127 -1
- realign/db/sqlite_db.py +617 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.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.10.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=cxZ-7zshfDXh7ZfRZqr7NrX86rO2tjfUbtVcF9Ls0BQ,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=ctQWoz883jXx_whObCoB58vhJjGAVUxld9sOZ371Tn8,13821
|
|
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=tq-PLwPLoadP8m_QN3WlQvPP_wi-rLG881Z8-tINjxo,5224
|
|
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=-8LEupS7e4PzwwQ5uwOYV-EzrXaprypQObw8OP1MgG4,26380
|
|
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=RPSVDAM-mQMAyTar3-XGrVUQIoCrMprk9tGa-AoZL_A,9421
|
|
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=S0BPY8V823ttPWk69W5PO6nu0DQDVHvYY0UObGI9ePo,23481
|
|
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.10.dist-info/METADATA,sha256=efo6zCepaEc3fBPRcQAi6QL9k5Y-gUee8RfSQBxbRwE,1598
|
|
93
|
+
aline_ai-0.5.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
94
|
+
aline_ai-0.5.10.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
95
|
+
aline_ai-0.5.10.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
96
|
+
aline_ai-0.5.10.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,66 @@ def _read_json(path: Path) -> dict[str, Any]:
|
|
|
34
34
|
return {}
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _write_to_db(
|
|
38
|
+
*,
|
|
39
|
+
terminal_id: str,
|
|
40
|
+
provider: str,
|
|
41
|
+
session_type: str,
|
|
42
|
+
session_id: str,
|
|
43
|
+
transcript_path: str = "",
|
|
44
|
+
cwd: str = "",
|
|
45
|
+
project_dir: str = "",
|
|
46
|
+
source: str = "",
|
|
47
|
+
context_id: Optional[str] = None,
|
|
48
|
+
attention: Optional[str] = None,
|
|
49
|
+
) -> bool:
|
|
50
|
+
"""Write terminal mapping to database (best-effort).
|
|
51
|
+
|
|
52
|
+
Returns True if successful, False otherwise.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
56
|
+
|
|
57
|
+
db_path = Path.home() / ".aline" / "realign.db"
|
|
58
|
+
db = SQLiteDatabase(str(db_path))
|
|
59
|
+
db.initialize()
|
|
60
|
+
|
|
61
|
+
# Check if agent exists
|
|
62
|
+
existing = db.get_agent_by_id(terminal_id)
|
|
63
|
+
if existing:
|
|
64
|
+
# Update existing agent
|
|
65
|
+
db.update_agent(
|
|
66
|
+
terminal_id,
|
|
67
|
+
provider=provider,
|
|
68
|
+
session_type=session_type,
|
|
69
|
+
session_id=session_id if session_id else None,
|
|
70
|
+
transcript_path=transcript_path if transcript_path else None,
|
|
71
|
+
cwd=cwd if cwd else None,
|
|
72
|
+
project_dir=project_dir if project_dir else None,
|
|
73
|
+
source=source if source else None,
|
|
74
|
+
context_id=context_id,
|
|
75
|
+
attention=attention,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
# Create new agent
|
|
79
|
+
db.get_or_create_agent(
|
|
80
|
+
terminal_id,
|
|
81
|
+
provider=provider,
|
|
82
|
+
session_type=session_type,
|
|
83
|
+
session_id=session_id if session_id else None,
|
|
84
|
+
context_id=context_id,
|
|
85
|
+
transcript_path=transcript_path if transcript_path else None,
|
|
86
|
+
cwd=cwd if cwd else None,
|
|
87
|
+
project_dir=project_dir if project_dir else None,
|
|
88
|
+
source=source if source else None,
|
|
89
|
+
attention=attention,
|
|
90
|
+
)
|
|
91
|
+
db.close()
|
|
92
|
+
return True
|
|
93
|
+
except Exception:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
37
97
|
def update_terminal_mapping(
|
|
38
98
|
*,
|
|
39
99
|
terminal_id: str,
|
|
@@ -44,11 +104,32 @@ def update_terminal_mapping(
|
|
|
44
104
|
cwd: str = "",
|
|
45
105
|
project_dir: str = "",
|
|
46
106
|
source: str = "",
|
|
107
|
+
context_id: Optional[str] = None,
|
|
108
|
+
attention: Optional[str] = None,
|
|
47
109
|
) -> None:
|
|
48
|
-
"""Update
|
|
110
|
+
"""Update terminal->session binding.
|
|
111
|
+
|
|
112
|
+
Writes to both:
|
|
113
|
+
1. SQLite database (primary storage, V15+)
|
|
114
|
+
2. ~/.aline/terminal.json (backward compatibility fallback)
|
|
49
115
|
|
|
50
|
-
Concurrency: uses a simple fcntl lock file; last writer wins, but updates are atomic.
|
|
116
|
+
Concurrency: uses a simple fcntl lock file for JSON; last writer wins, but updates are atomic.
|
|
51
117
|
"""
|
|
118
|
+
# Phase 1: Write to database (best-effort, don't fail if DB unavailable)
|
|
119
|
+
_write_to_db(
|
|
120
|
+
terminal_id=terminal_id,
|
|
121
|
+
provider=provider,
|
|
122
|
+
session_type=session_type,
|
|
123
|
+
session_id=session_id,
|
|
124
|
+
transcript_path=transcript_path,
|
|
125
|
+
cwd=cwd,
|
|
126
|
+
project_dir=project_dir,
|
|
127
|
+
source=source,
|
|
128
|
+
context_id=context_id,
|
|
129
|
+
attention=attention,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Phase 2: Write to JSON (backward compatibility)
|
|
52
133
|
state_path = _state_path()
|
|
53
134
|
lock_path = _lock_path()
|
|
54
135
|
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -74,6 +155,8 @@ def update_terminal_mapping(
|
|
|
74
155
|
"cwd": cwd,
|
|
75
156
|
"project_dir": project_dir,
|
|
76
157
|
"source": source,
|
|
158
|
+
"context_id": context_id,
|
|
159
|
+
"attention": attention,
|
|
77
160
|
"updated_at": time.time(),
|
|
78
161
|
}
|
|
79
162
|
|
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,19 @@ 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.sqlite_db import SQLiteDatabase
|
|
27
|
+
|
|
28
|
+
db_path = Path.home() / ".aline" / "realign.db"
|
|
29
|
+
db = SQLiteDatabase(str(db_path))
|
|
30
|
+
db.initialize()
|
|
31
|
+
return db
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
22
36
|
@dataclass
|
|
23
37
|
class ContextEntry:
|
|
24
38
|
"""A single context entry in load.json."""
|
|
@@ -78,15 +92,54 @@ class ContextConfig:
|
|
|
78
92
|
return cls(contexts=contexts, default=default)
|
|
79
93
|
|
|
80
94
|
|
|
95
|
+
def _load_context_config_from_db() -> Optional[ContextConfig]:
|
|
96
|
+
"""Load context configuration from database (best-effort)."""
|
|
97
|
+
try:
|
|
98
|
+
db = _get_db()
|
|
99
|
+
if not db:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
db_contexts = db.list_agent_contexts(limit=100)
|
|
103
|
+
if not db_contexts:
|
|
104
|
+
db.close()
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
contexts = []
|
|
108
|
+
for ctx in db_contexts:
|
|
109
|
+
entry = ContextEntry(
|
|
110
|
+
context_sessions=ctx.session_ids or [],
|
|
111
|
+
context_events=ctx.event_ids or [],
|
|
112
|
+
context_id=ctx.id,
|
|
113
|
+
workspace=ctx.workspace,
|
|
114
|
+
loaded_at=ctx.loaded_at,
|
|
115
|
+
)
|
|
116
|
+
contexts.append(entry)
|
|
117
|
+
|
|
118
|
+
db.close()
|
|
119
|
+
return ContextConfig(contexts=contexts)
|
|
120
|
+
except Exception:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
81
124
|
def load_context_config(path: Optional[Path] = None) -> Optional[ContextConfig]:
|
|
82
|
-
"""Load context configuration
|
|
125
|
+
"""Load context configuration.
|
|
126
|
+
|
|
127
|
+
Priority:
|
|
128
|
+
1. SQLite database (primary storage, V15+)
|
|
129
|
+
2. ~/.aline/load.json (fallback for backward compatibility)
|
|
83
130
|
|
|
84
131
|
Args:
|
|
85
132
|
path: Optional path to load.json. Defaults to ~/.aline/load.json
|
|
86
133
|
|
|
87
134
|
Returns:
|
|
88
|
-
ContextConfig if
|
|
135
|
+
ContextConfig if data exists and is valid, None otherwise.
|
|
89
136
|
"""
|
|
137
|
+
# Phase 1: Try to load from database
|
|
138
|
+
db_config = _load_context_config_from_db()
|
|
139
|
+
if db_config and db_config.contexts:
|
|
140
|
+
return db_config
|
|
141
|
+
|
|
142
|
+
# Phase 2: Fall back to JSON file
|
|
90
143
|
config_path = path or LOAD_JSON_PATH
|
|
91
144
|
|
|
92
145
|
if not config_path.exists():
|
|
@@ -100,8 +153,55 @@ def load_context_config(path: Optional[Path] = None) -> Optional[ContextConfig]:
|
|
|
100
153
|
return None
|
|
101
154
|
|
|
102
155
|
|
|
156
|
+
def _sync_context_to_db(entry: ContextEntry) -> bool:
|
|
157
|
+
"""Sync a context entry to the database (best-effort)."""
|
|
158
|
+
if not entry.context_id:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
db = _get_db()
|
|
163
|
+
if not db:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
# Get or create the context
|
|
167
|
+
existing = db.get_agent_context_by_id(entry.context_id)
|
|
168
|
+
if existing:
|
|
169
|
+
db.update_agent_context(
|
|
170
|
+
entry.context_id,
|
|
171
|
+
workspace=entry.workspace,
|
|
172
|
+
loaded_at=entry.loaded_at,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
db.get_or_create_agent_context(
|
|
176
|
+
entry.context_id,
|
|
177
|
+
workspace=entry.workspace,
|
|
178
|
+
loaded_at=entry.loaded_at,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Update session links
|
|
182
|
+
if entry.context_sessions:
|
|
183
|
+
db.set_agent_context_sessions(entry.context_id, entry.context_sessions)
|
|
184
|
+
else:
|
|
185
|
+
db.set_agent_context_sessions(entry.context_id, [])
|
|
186
|
+
|
|
187
|
+
# Update event links
|
|
188
|
+
if entry.context_events:
|
|
189
|
+
db.set_agent_context_events(entry.context_id, entry.context_events)
|
|
190
|
+
else:
|
|
191
|
+
db.set_agent_context_events(entry.context_id, [])
|
|
192
|
+
|
|
193
|
+
db.close()
|
|
194
|
+
return True
|
|
195
|
+
except Exception:
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
|
|
103
199
|
def save_context_config(config: ContextConfig, path: Optional[Path] = None) -> bool:
|
|
104
|
-
"""Save context configuration
|
|
200
|
+
"""Save context configuration.
|
|
201
|
+
|
|
202
|
+
Writes to both:
|
|
203
|
+
1. SQLite database (primary storage, V15+)
|
|
204
|
+
2. ~/.aline/load.json (backward compatibility fallback)
|
|
105
205
|
|
|
106
206
|
Args:
|
|
107
207
|
config: ContextConfig to save
|
|
@@ -110,6 +210,13 @@ def save_context_config(config: ContextConfig, path: Optional[Path] = None) -> b
|
|
|
110
210
|
Returns:
|
|
111
211
|
True if successful, False otherwise.
|
|
112
212
|
"""
|
|
213
|
+
# Phase 1: Sync all contexts to database (best-effort)
|
|
214
|
+
for entry in config.contexts:
|
|
215
|
+
_sync_context_to_db(entry)
|
|
216
|
+
if config.default and config.default.context_id:
|
|
217
|
+
_sync_context_to_db(config.default)
|
|
218
|
+
|
|
219
|
+
# Phase 2: Write to JSON (backward compatibility)
|
|
113
220
|
config_path = path or LOAD_JSON_PATH
|
|
114
221
|
|
|
115
222
|
# Ensure parent directory exists
|
|
@@ -317,6 +424,11 @@ def get_context_by_id(
|
|
|
317
424
|
) -> Optional[ContextEntry]:
|
|
318
425
|
"""Get a specific context by its ID.
|
|
319
426
|
|
|
427
|
+
Priority:
|
|
428
|
+
1. SQLite database (primary storage, V15+)
|
|
429
|
+
2. In-memory config if provided
|
|
430
|
+
3. ~/.aline/load.json (fallback)
|
|
431
|
+
|
|
320
432
|
Args:
|
|
321
433
|
context_id: The context ID to look up
|
|
322
434
|
config: Existing config to search
|
|
@@ -325,6 +437,24 @@ def get_context_by_id(
|
|
|
325
437
|
Returns:
|
|
326
438
|
The ContextEntry if found, None otherwise.
|
|
327
439
|
"""
|
|
440
|
+
# Phase 1: Try to load from database
|
|
441
|
+
try:
|
|
442
|
+
db = _get_db()
|
|
443
|
+
if db:
|
|
444
|
+
ctx = db.get_agent_context_by_id(context_id)
|
|
445
|
+
db.close()
|
|
446
|
+
if ctx:
|
|
447
|
+
return ContextEntry(
|
|
448
|
+
context_sessions=ctx.session_ids or [],
|
|
449
|
+
context_events=ctx.event_ids or [],
|
|
450
|
+
context_id=ctx.id,
|
|
451
|
+
workspace=ctx.workspace,
|
|
452
|
+
loaded_at=ctx.loaded_at,
|
|
453
|
+
)
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
# Phase 2: Check in-memory config or load from JSON
|
|
328
458
|
if config is None:
|
|
329
459
|
config = load_context_config(path)
|
|
330
460
|
|
|
@@ -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,46 @@ 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.sqlite_db import SQLiteDatabase
|
|
198
|
+
|
|
199
|
+
db_path = Path.home() / ".aline" / "realign.db"
|
|
200
|
+
db = SQLiteDatabase(str(db_path), read_only=True)
|
|
201
|
+
|
|
202
|
+
agents = db.list_agents(status="active", limit=100)
|
|
203
|
+
db.close()
|
|
204
|
+
|
|
205
|
+
out: dict[str, dict[str, str]] = {}
|
|
206
|
+
for agent in agents:
|
|
207
|
+
data: dict[str, str] = {}
|
|
208
|
+
if agent.provider:
|
|
209
|
+
data["provider"] = agent.provider
|
|
210
|
+
if agent.session_type:
|
|
211
|
+
data["session_type"] = agent.session_type
|
|
212
|
+
if agent.session_id:
|
|
213
|
+
data["session_id"] = agent.session_id
|
|
214
|
+
if agent.transcript_path:
|
|
215
|
+
data["transcript_path"] = agent.transcript_path
|
|
216
|
+
if agent.cwd:
|
|
217
|
+
data["cwd"] = agent.cwd
|
|
218
|
+
if agent.project_dir:
|
|
219
|
+
data["project_dir"] = agent.project_dir
|
|
220
|
+
if agent.source:
|
|
221
|
+
data["source"] = agent.source
|
|
222
|
+
if agent.context_id:
|
|
223
|
+
data["context_id"] = agent.context_id
|
|
224
|
+
if agent.attention:
|
|
225
|
+
data["attention"] = agent.attention
|
|
226
|
+
out[agent.id] = data
|
|
227
|
+
return out
|
|
228
|
+
except Exception:
|
|
229
|
+
return {}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _load_terminal_state_from_json() -> dict[str, dict[str, str]]:
|
|
233
|
+
"""Load terminal state from JSON file (fallback)."""
|
|
196
234
|
try:
|
|
197
235
|
path = Path.home() / ".aline" / "terminal.json"
|
|
198
236
|
if not path.exists():
|
|
@@ -212,6 +250,28 @@ def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
|
212
250
|
return {}
|
|
213
251
|
|
|
214
252
|
|
|
253
|
+
def _load_terminal_state() -> dict[str, dict[str, str]]:
|
|
254
|
+
"""Load terminal state.
|
|
255
|
+
|
|
256
|
+
Priority:
|
|
257
|
+
1. SQLite database (primary storage, V15+)
|
|
258
|
+
2. ~/.aline/terminal.json (fallback for backward compatibility)
|
|
259
|
+
|
|
260
|
+
Merges both sources, with DB taking precedence.
|
|
261
|
+
"""
|
|
262
|
+
# Phase 1: Load from database
|
|
263
|
+
db_state = _load_terminal_state_from_db()
|
|
264
|
+
|
|
265
|
+
# Phase 2: Load from JSON as fallback
|
|
266
|
+
json_state = _load_terminal_state_from_json()
|
|
267
|
+
|
|
268
|
+
# Merge: DB takes precedence, JSON provides fallback for entries not in DB
|
|
269
|
+
result = dict(json_state)
|
|
270
|
+
result.update(db_state)
|
|
271
|
+
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
|
|
215
275
|
def _aline_tmux_conf_path() -> Path:
|
|
216
276
|
return Path.home() / ".aline" / "tmux" / "tmux.conf"
|
|
217
277
|
|
|
@@ -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",
|