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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.5.9
3
+ Version: 0.5.10
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.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=S1YEUn5HWSDTerDDMsSsRV871IZxoaxDjPTPI2z6-Xs,9976
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=ZvdQ-ZmqEltdMoNk3lXVsbpvbAQEmf2hxTCY_8WFu9g,2586
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=EfhmB5FF9MH1rtM-ICM0BFJ25kBg6mlMbmA4t4tcVRA,24409
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=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=RPSVDAM-mQMAyTar3-XGrVUQIoCrMprk9tGa-AoZL_A,9421
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=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.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.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
@@ -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.10"
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,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 ~/.aline/terminal.json with the latest terminal->session binding.
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 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,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 from load.json.
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 file exists and is valid, None otherwise.
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 to load.json.
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["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,46 @@ 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.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(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",