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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.5.9
3
+ Version: 0.5.11
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,10 +1,10 @@
1
- aline_ai-0.5.9.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=OLwYTOwCSddSWbQNVsKd5oap0zIeASs5UyR2ZpBXeP0,1623
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=S1YEUn5HWSDTerDDMsSsRV871IZxoaxDjPTPI2z6-Xs,9976
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=ZvdQ-ZmqEltdMoNk3lXVsbpvbAQEmf2hxTCY_8WFu9g,2586
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=EfhmB5FF9MH1rtM-ICM0BFJ25kBg6mlMbmA4t4tcVRA,24409
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=ugEs3IHrT7FsbuMEwyrqY3eoylp_pbftw42_Fu07tF4,7419
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=YreJ9EiU3OA1qAfcWZ9XuptvCJe8-REKcr03sm7dY-Q,28751
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=-1d-Zc4IOUVokbdTXi3R-bIwlkFEPAz_qTHAdcsdp6g,1870
68
- realign/db/base.py,sha256=4OkwPi6qL_8ZJb1ATNkHr-JaIxh98UYTSZ6fSYFff6s,12033
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=Qj8nRs7plc8MXXTq7D4vi4L0joaiEjaI0mZMzUC4z78,18066
72
- realign/db/sqlite_db.py,sha256=UmUjo3OW7F6YEeOSdl0-fGOXNFn_tC7d3EYEEUNzNZU,81793
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.9.dist-info/METADATA,sha256=sh6dCwAskyh891skkjvCNomqfJ2RoYTLWPzccHRpUt8,1597
92
- aline_ai-0.5.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
93
- aline_ai-0.5.9.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
94
- aline_ai-0.5.9.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
95
- aline_ai-0.5.9.dist-info/RECORD,,
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
@@ -3,7 +3,7 @@
3
3
  import hashlib
4
4
  from pathlib import Path
5
5
 
6
- __version__ = "0.5.9"
6
+ __version__ = "0.5.11"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
@@ -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 ~/.aline/terminal.json with the latest terminal->session binding.
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 the ~/.aline/load.json file which allows users to
4
- pre-define search contexts (sets of sessions/events) that can be loaded
5
- to limit search scope.
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 from load.json.
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 file exists and is valid, None otherwise.
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 to load.json.
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["last_workspace"] = path
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 _load_terminal_state() -> dict[str, dict[str, str]]:
195
- """Load ~/.aline/terminal.json (best-effort)."""
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(self, agent_type: str, workspace: str) -> None:
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(self, workspace: str) -> None:
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("claude"), workspace
719
+ tmux_manager.zsh_run_and_keep_open(claude_cmd), workspace
713
720
  )
714
721
  created = tmux_manager.create_inner_window(
715
722
  "cc",