aline-ai 0.6.1__py3-none-any.whl → 0.6.2__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.6.1
3
+ Version: 0.6.2
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,8 +1,8 @@
1
- aline_ai-0.6.1.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=ohwpTX-XJAcf8rW1lcIJqXudQQ72HJvA9d0RsAJhURU,1623
1
+ aline_ai-0.6.2.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=WCultxtZIKlfMhrDP35gb3Jtyvc7vOLpx-I1JO6nPAc,1623
3
3
  realign/auth.py,sha256=d_1yvCwluN5iIrdgjtuSKpOYAksDzrzNgntKacLVJrw,16583
4
4
  realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
5
- realign/cli.py,sha256=HIKpLIx7lwHV-Gd3cbdZVaOst784or6asQdAI-DFE80,43543
5
+ realign/cli.py,sha256=M8lpAkDaE2ZjESzrlksFk8M8cpbFKkIzK3y8qNnZmJo,43793
6
6
  realign/codex_detector.py,sha256=N9ulgMgvTzDfXE4s4vLd6OoS0hT7R6h2bDFFXWa-2hE,4183
7
7
  realign/config.py,sha256=d8HQ6v1xju6cjNGbR7LlfHaMyvFPcMDofhGbepxpQq8,11634
8
8
  realign/context.py,sha256=8hzgNOg-7_eMW22wt7OM5H9IsmMveKXCv0epG7E0G7w,13917
@@ -13,9 +13,9 @@ realign/logging_config.py,sha256=LCAigKFhTj86PSJm4-kUl3Ag9h_GENh3x2iPnMv7qUI,487
13
13
  realign/mcp_server.py,sha256=LWiQ2qukYoNLsoV2ID2f0vF9jkJlBvB587HpM5jymgE,10193
14
14
  realign/mcp_watcher.py,sha256=aK4jWStv7CoCroS4tXFHgZ_y_-q4QDjrpWgm4DxcEj4,1260
15
15
  realign/redactor.py,sha256=Zsoi5HfYak2yPmck20JArhm-1cPSB78IdkBJiNVXfrc,17096
16
- realign/watcher_core.py,sha256=NNn_xlm50Ybb60--DrF9dyvzGgJ4ENQeAUbZsH7w4to,106555
16
+ realign/watcher_core.py,sha256=k9esacTBfHpa77MPEXAsiMDiFa4Ml52vWfGGT-IucAM,108135
17
17
  realign/watcher_daemon.py,sha256=OHUQ9P1LlagKJHfrf6uRnzO-zDtBRXIxt8ydMFHf5S8,3475
18
- realign/worker_core.py,sha256=-GOItHE0vzExB8LZK6KeHx4tt_mIqtCoUljOtEg2x8A,10105
18
+ realign/worker_core.py,sha256=TXioUVJlOO-8EgmKssCTLIyuh0aaupRLb1sh9s3kSuc,10194
19
19
  realign/worker_daemon.py,sha256=X7Xyjw_u6m6KG4E84nx0HpDFw4cWMv8ja1G8btc9PiM,3957
20
20
  realign/adapters/__init__.py,sha256=bpDm5aBxMdq4OA_beYahoUb4zfNaq3KOG6KghQJruRc,827
21
21
  realign/adapters/antigravity.py,sha256=geaYxAEswpgsVtERqsQ1OwvPFsy5tRkyjx2yQ-Uq9nM,5461
@@ -27,10 +27,10 @@ realign/adapters/registry.py,sha256=yM6nf9nGTJ1vaK2Uixp-VacseK7PmxZkCdKedmWI8MA,
27
27
  realign/claude_hooks/__init__.py,sha256=MT9c8TWjLO23xDCM-uBBMy_mOThNd7O-AgN_Khn30qs,594
28
28
  realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFrooDEcd9KxtmF2-3nCw,6448
29
29
  realign/claude_hooks/permission_request_hook_installer.py,sha256=_8Wr_L5MES7iGukJzcaj4bqR0BH8kFL44U_X4iKtw2Y,7791
30
- realign/claude_hooks/stop_hook.py,sha256=2nzF2aF1p5teMJ0eV0ALEHD1K-yVj5sSh7UE8xL54ZE,12025
30
+ realign/claude_hooks/stop_hook.py,sha256=GWADlzaTGzV8_BUKLLGhHmwJDIXSLQGVUUBuP_rdJ0o,13431
31
31
  realign/claude_hooks/stop_hook_installer.py,sha256=uyqKOqpix7CQP64ERBvvh7viSPp_wx_JVGNAX18rKh0,7228
32
32
  realign/claude_hooks/terminal_state.py,sha256=i8B6b_2_9ttPEemp7SrGdFRJSa-vm5lc7YSTRTvAWNg,5397
33
- realign/claude_hooks/user_prompt_submit_hook.py,sha256=WD-UavhBTueN2TPfnZrnPC7DFYGEeptjUEF21EJn7Qo,10312
33
+ realign/claude_hooks/user_prompt_submit_hook.py,sha256=kMrmhAVtfV41oTX7JZcq2HPXjgQQ5gX26iOJoHJkfqA,10474
34
34
  realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
35
35
  realign/commands/__init__.py,sha256=sx_ck55oxaoiF4N3LugG0ZXwonUDxeEZ5uHbBKCC7K8,89
36
36
  realign/commands/add.py,sha256=njZgg3paUmOw-sb-sWkXr_eUaf5bD-hBEiRectaphPs,24332
@@ -46,15 +46,15 @@ realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,1
46
46
  realign/commands/watcher.py,sha256=rB3x2diC7scqghl7wLAeBNgMj4NLXUKbr0ou1_VVUIU,136292
47
47
  realign/commands/worker.py,sha256=jTu7Pj60nTnn7SsH3oNCNnO6zl4TIFCJVNSC1OoQ_0o,23363
48
48
  realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
49
- realign/dashboard/app.py,sha256=l4qbjQfnUJCzX2M0A5vPJ-fTX-Tz2Ykw4_cY0eMC1LE,16029
49
+ realign/dashboard/app.py,sha256=yy2rjMQuAfpukFgrA6udAnAbCXQoAHr9WeeuZ9bCSa4,16064
50
50
  realign/dashboard/layout.py,sha256=sZxmFj6QTbkois9MHTvBEMMcnaRVehCDqugdbiFx10k,9072
51
51
  realign/dashboard/terminal_backend.py,sha256=MlDfwtqhftyQK6jDNizQGFjAWIo5Bx2TDpSnP3MCZVM,3375
52
- realign/dashboard/tmux_manager.py,sha256=Vt_30WNtDg7c_9SEh8xdDtBLJ8kNq6bGSPh5r3VXpg0,26276
52
+ realign/dashboard/tmux_manager.py,sha256=K8sjzSBtISLuWF7s4g4YieJ4oiE5wIgSztFvmhJZd0M,26849
53
53
  realign/dashboard/backends/__init__.py,sha256=POROX7YKtukYZcLB1pi_kO0sSEpuO3y-hwmF3WIN1Kk,163
54
54
  realign/dashboard/backends/iterm2.py,sha256=XYYJT5lrrp4pW_MyEqPZYkRI0qyKUwJlezwMidgnsHc,21390
55
55
  realign/dashboard/backends/kitty.py,sha256=5jdkR1f2PwB8a4SnS3EG6uOQ2XU-PB7-cpKBfIJq3hU,12066
56
56
  realign/dashboard/screens/__init__.py,sha256=US6sAmQs5VVkH2tFkH_z0WDT4H8cVhLL-JckfSR1yQY,446
57
- realign/dashboard/screens/create_agent.py,sha256=lpcT1zLq_p02codtHTE8KdbEzCEaNLnk1lqU3QLcXCg,10057
57
+ realign/dashboard/screens/create_agent.py,sha256=06uiQYvz-Xvn4Xm689o3tdhzb2HQ0gdzAA1WHVEwziM,11706
58
58
  realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
59
59
  realign/dashboard/screens/event_detail.py,sha256=OLaL3-FgAohDdzVlfuUw5yh2SR49IHIpCtiqXJhBTc0,20992
60
60
  realign/dashboard/screens/help_screen.py,sha256=Icrcvbgyz49R2tBiu8vBZ4CLm6iYclv_-FTa2pCFRRQ,3398
@@ -68,7 +68,7 @@ realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMW
68
68
  realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
69
69
  realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
70
70
  realign/dashboard/widgets/sessions_table.py,sha256=GyaWzvt-elKx1iE2YR94CBNPyjqM8B7g0j7zsukiXi0,36517
71
- realign/dashboard/widgets/terminal_panel.py,sha256=7VrLN_Wsk98O0rojckrqJ_k5TGM47hP4UqXFvuRTvRs,45097
71
+ realign/dashboard/widgets/terminal_panel.py,sha256=BZgh7lo9rhuJdvSGBhUPlWjS3KjcXDJP7SSnNngmFaY,45805
72
72
  realign/dashboard/widgets/watcher_panel.py,sha256=O_mdDacgc87xA-5KEfta53Ik_Xsk_B2OfwenMOTtGw8,19722
73
73
  realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
74
74
  realign/db/__init__.py,sha256=65LsNdsq_rkwNC1eg1OAr3HC0ORXtelOh0I8MhNGr-g,3288
@@ -77,11 +77,11 @@ realign/db/locks.py,sha256=yzCiPJZ4eOQX-Q4mXB6s76U2U7lXAzIBBy1t59w-AVU,1698
77
77
  realign/db/migrate_agents.py,sha256=cDeVUzKW950dJ0lV74QObHuONqKwErSrXI5akU2vBmQ,9633
78
78
  realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
79
79
  realign/db/schema.py,sha256=YHj5PGZWbCl0VG0epnMF_Ofg3jRiLHq6SLHCi1q34eQ,30181
80
- realign/db/sqlite_db.py,sha256=0tB_chSBr44_VTFLe5_hmQk5xUuNym9JZLFSAAE9DNk,104225
80
+ realign/db/sqlite_db.py,sha256=AJb05gWN8L7K79-f9TJ0UGqGnoHUED1z-yjufGQTyQE,104929
81
81
  realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
82
82
  realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
83
83
  realign/events/event_summarizer.py,sha256=ZLiwOXWN8eawep3cQs3Wh9QLSypvU1SRbe8GTJXJQaY,8272
84
- realign/events/session_summarizer.py,sha256=IWYcDHGbsPtZEeDcQMWy4V-IKi5QBqpA5uuOIGy4Sls,10386
84
+ realign/events/session_summarizer.py,sha256=W5J1zZs1ZrH0MvWgvPObwB1KFdxXEZMLctAmFQC_rok,10994
85
85
  realign/models/event.py,sha256=ypz74D4l6U2U0RhgL8fzEhiq7iQjhHybmAdLUNDY7P4,5521
86
86
  realign/prompts/__init__.py,sha256=PpYR7f-T96fd-QyNYJDRS1U6h9O0rIt_SMsREy9i3aA,443
87
87
  realign/prompts/presets.py,sha256=h9oEy0XP4JQ4DCnp8HN_FfF0LmI-yOV6xWJLknIghJ8,7256
@@ -96,8 +96,8 @@ realign/triggers/next_turn_trigger.py,sha256=BpP0PWn4mU1MZd6mv89jWcjs8Jtv0zEWapW
96
96
  realign/triggers/registry.py,sha256=cb-AVLbYB2pqwfWL3q1DQxLv4kOw7g7m-GshTdfFESc,3827
97
97
  realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
98
98
  realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
99
- aline_ai-0.6.1.dist-info/METADATA,sha256=ftCsc3aI1fjTZhCJd8rE_7-teyrN8ARYIyEmDGMtJdg,1597
100
- aline_ai-0.6.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
101
- aline_ai-0.6.1.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
102
- aline_ai-0.6.1.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
103
- aline_ai-0.6.1.dist-info/RECORD,,
99
+ aline_ai-0.6.2.dist-info/METADATA,sha256=KJbwjx2uZdXBhG2HyeVUs77RT72msrkJLRYBlCrldVc,1597
100
+ aline_ai-0.6.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
101
+ aline_ai-0.6.2.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
102
+ aline_ai-0.6.2.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
103
+ aline_ai-0.6.2.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.6.1"
6
+ __version__ = "0.6.2"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
@@ -98,6 +98,9 @@ def main():
98
98
  signal_file = signal_dir / f"{session_id}_{timestamp_ms}.signal"
99
99
  tmp_file = signal_dir / f"{session_id}_{timestamp_ms}.signal.tmp"
100
100
 
101
+ # Check for no-track mode
102
+ no_track = os.environ.get("ALINE_NO_TRACK", "") == "1"
103
+
101
104
  signal_data = {
102
105
  "session_id": session_id,
103
106
  "terminal_id": terminal_id,
@@ -107,6 +110,8 @@ def main():
107
110
  "timestamp": time.time(),
108
111
  "hook_event": "Stop",
109
112
  }
113
+ if no_track:
114
+ signal_data["no_track"] = True
110
115
 
111
116
  # Write atomically to avoid watcher reading a partial JSON file.
112
117
  tmp_file.write_text(json.dumps(signal_data, indent=2))
@@ -193,6 +198,22 @@ def main():
193
198
  ],
194
199
  check=False,
195
200
  )
201
+ # Set no-track flag if applicable
202
+ if no_track:
203
+ subprocess.run(
204
+ [
205
+ "tmux",
206
+ "-L",
207
+ inner_socket,
208
+ "set-option",
209
+ "-w",
210
+ "-t",
211
+ window_id,
212
+ "@aline_no_track",
213
+ "1",
214
+ ],
215
+ check=False,
216
+ )
196
217
  if transcript_path:
197
218
  subprocess.run(
198
219
  [
@@ -285,6 +306,20 @@ def main():
285
306
  ],
286
307
  check=False,
287
308
  )
309
+ # Set no-track flag if applicable
310
+ if no_track:
311
+ subprocess.run(
312
+ [
313
+ "tmux",
314
+ "set-option",
315
+ "-w",
316
+ "-t",
317
+ window_id,
318
+ "@aline_no_track",
319
+ "1",
320
+ ],
321
+ check=False,
322
+ )
288
323
  if transcript_path:
289
324
  subprocess.run(
290
325
  [
@@ -80,6 +80,9 @@ def main() -> None:
80
80
  signal_file = signal_dir / f"{session_id}_{timestamp_ms}.signal"
81
81
  tmp_file = signal_dir / f"{session_id}_{timestamp_ms}.signal.tmp"
82
82
 
83
+ # Check for no-track mode
84
+ no_track = os.environ.get("ALINE_NO_TRACK", "") == "1"
85
+
83
86
  signal_data = {
84
87
  "session_id": session_id,
85
88
  "terminal_id": terminal_id,
@@ -90,6 +93,8 @@ def main() -> None:
90
93
  "timestamp": time.time(),
91
94
  "hook_event": "UserPromptSubmit",
92
95
  }
96
+ if no_track:
97
+ signal_data["no_track"] = True
93
98
 
94
99
  tmp_file.write_text(json.dumps(signal_data, indent=2))
95
100
  tmp_file.replace(signal_file)
realign/cli.py CHANGED
@@ -131,8 +131,8 @@ def doctor_cli(
131
131
  - Clears Python bytecode cache (.pyc files)
132
132
  - Updates Claude Code hooks (Stop, UserPromptSubmit, PermissionRequest)
133
133
  - Updates skills to latest version
134
- - Restarts the watcher daemon (if running)
135
- - Restarts the worker daemon (if running)
134
+ - Ensures watcher daemon is running (restarts if running, starts if not)
135
+ - Ensures worker daemon is running (restarts if running, starts if not)
136
136
 
137
137
  Run this after pulling new code to ensure everything uses the latest version.
138
138
  """
@@ -285,20 +285,22 @@ def doctor_cli(
285
285
 
286
286
  if watcher_was_running:
287
287
  console.print(" [dim]Starting watcher daemon...[/dim]")
288
- try:
289
- subprocess.Popen(
290
- ["python", "-m", "src.realign.watcher_daemon"],
291
- stdout=subprocess.DEVNULL,
292
- stderr=subprocess.DEVNULL,
293
- start_new_session=True,
294
- cwd=str(project_root),
295
- )
296
- time.sleep(2)
297
- console.print(" [green]✓[/green] Watcher daemon restarted")
298
- except Exception as e:
299
- console.print(f" [red]✗[/red] Failed to restart watcher: {e}")
300
288
  else:
301
- console.print(" [dim]Watcher daemon was not running[/dim]")
289
+ console.print(" [dim]Watcher daemon was not running, starting...[/dim]")
290
+ try:
291
+ subprocess.Popen(
292
+ ["python", "-m", "src.realign.watcher_daemon"],
293
+ stdout=subprocess.DEVNULL,
294
+ stderr=subprocess.DEVNULL,
295
+ start_new_session=True,
296
+ cwd=str(project_root),
297
+ )
298
+ time.sleep(2)
299
+ action = "restarted" if watcher_was_running else "started"
300
+ console.print(f" [green]✓[/green] Watcher daemon {action}")
301
+ except Exception as e:
302
+ action = "restart" if watcher_was_running else "start"
303
+ console.print(f" [red]✗[/red] Failed to {action} watcher: {e}")
302
304
 
303
305
  # 5. Restart worker daemon
304
306
  console.print("\n[bold]5. Checking worker daemon...[/bold]")
@@ -329,20 +331,22 @@ def doctor_cli(
329
331
 
330
332
  if worker_was_running:
331
333
  console.print(" [dim]Starting worker daemon...[/dim]")
332
- try:
333
- subprocess.Popen(
334
- ["python", "-m", "src.realign.worker_daemon"],
335
- stdout=subprocess.DEVNULL,
336
- stderr=subprocess.DEVNULL,
337
- start_new_session=True,
338
- cwd=str(project_root),
339
- )
340
- time.sleep(2)
341
- console.print(" [green]✓[/green] Worker daemon restarted")
342
- except Exception as e:
343
- console.print(f" [red]✗[/red] Failed to restart worker: {e}")
344
334
  else:
345
- console.print(" [dim]Worker daemon was not running[/dim]")
335
+ console.print(" [dim]Worker daemon was not running, starting...[/dim]")
336
+ try:
337
+ subprocess.Popen(
338
+ ["python", "-m", "src.realign.worker_daemon"],
339
+ stdout=subprocess.DEVNULL,
340
+ stderr=subprocess.DEVNULL,
341
+ start_new_session=True,
342
+ cwd=str(project_root),
343
+ )
344
+ time.sleep(2)
345
+ action = "restarted" if worker_was_running else "started"
346
+ console.print(f" [green]✓[/green] Worker daemon {action}")
347
+ except Exception as e:
348
+ action = "restart" if worker_was_running else "start"
349
+ console.print(f" [red]✗[/red] Failed to {action} worker: {e}")
346
350
 
347
351
  console.print("\n[green]Done![/green] Aline is ready with the latest code.")
348
352
  raise typer.Exit(code=0)
realign/dashboard/app.py CHANGED
@@ -61,6 +61,7 @@ class AlineDashboard(App):
61
61
 
62
62
  CSS_PATH = "styles/dashboard.tcss"
63
63
  TITLE = "Aline Dashboard"
64
+ ENABLE_COMMAND_PALETTE = False
64
65
 
65
66
  BINDINGS = [
66
67
  Binding("r", "refresh", "Refresh", show=False),
@@ -60,6 +60,23 @@ def _save_claude_permission_mode(mode: str) -> None:
60
60
  _save_state("claude_permission_mode", mode)
61
61
 
62
62
 
63
+ def _load_claude_tracking_mode() -> str:
64
+ """Load the last used Claude tracking mode from state file."""
65
+ try:
66
+ if DASHBOARD_STATE_FILE.exists():
67
+ with open(DASHBOARD_STATE_FILE, "r", encoding="utf-8") as f:
68
+ state = json.load(f)
69
+ return state.get("claude_tracking_mode", "track")
70
+ except Exception:
71
+ pass
72
+ return "track"
73
+
74
+
75
+ def _save_claude_tracking_mode(mode: str) -> None:
76
+ """Save the Claude tracking mode to state file."""
77
+ _save_state("claude_tracking_mode", mode)
78
+
79
+
63
80
  def _save_state(key: str, value: str) -> None:
64
81
  """Save a key-value pair to the state file."""
65
82
  try:
@@ -75,10 +92,10 @@ def _save_state(key: str, value: str) -> None:
75
92
  pass
76
93
 
77
94
 
78
- class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
95
+ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool, bool]]]):
79
96
  """Modal to create a new agent terminal.
80
97
 
81
- Returns a tuple of (agent_type, workspace_path, skip_permissions) on success, None on cancel.
98
+ Returns a tuple of (agent_type, workspace_path, skip_permissions, no_track) on success, None on cancel.
82
99
  """
83
100
 
84
101
  BINDINGS = [
@@ -175,6 +192,7 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
175
192
  super().__init__()
176
193
  self._workspace_path = _load_last_workspace()
177
194
  self._permission_mode = _load_claude_permission_mode()
195
+ self._tracking_mode = _load_claude_tracking_mode()
178
196
 
179
197
  def compose(self) -> ComposeResult:
180
198
  with Container(id="create-agent-root"):
@@ -199,6 +217,11 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
199
217
  yield RadioButton("Normal", id="perm-normal", value=True)
200
218
  yield RadioButton("Skip (--dangerously-skip-permissions)", id="perm-skip")
201
219
 
220
+ yield Label("Tracking", classes="section-label")
221
+ with RadioSet(id="tracking-mode"):
222
+ yield RadioButton("Track", id="track-track", value=True)
223
+ yield RadioButton("No Track (skip LLM summaries)", id="track-notrack")
224
+
202
225
  with Horizontal(id="buttons"):
203
226
  yield Button("Cancel", id="cancel")
204
227
  yield Button("Create", id="create", variant="primary")
@@ -209,6 +232,11 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
209
232
  self.query_one("#perm-skip", RadioButton).value = True
210
233
  else:
211
234
  self.query_one("#perm-normal", RadioButton).value = True
235
+ # Set the saved tracking mode
236
+ if self._tracking_mode == "notrack":
237
+ self.query_one("#track-notrack", RadioButton).value = True
238
+ else:
239
+ self.query_one("#track-track", RadioButton).value = True
212
240
  self.query_one("#create", Button).focus()
213
241
 
214
242
  def action_close(self) -> None:
@@ -292,8 +320,9 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
292
320
  }
293
321
  agent_type = agent_type_map.get(pressed_button.id or "", "claude")
294
322
 
295
- # Get permission mode (only relevant for Claude)
323
+ # Get permission mode and tracking mode (only relevant for Claude)
296
324
  skip_permissions = False
325
+ no_track = False
297
326
  if agent_type == "claude":
298
327
  perm_radio_set = self.query_one("#permission-mode", RadioSet)
299
328
  perm_pressed = perm_radio_set.pressed_button
@@ -302,8 +331,16 @@ class CreateAgentScreen(ModalScreen[Optional[tuple[str, str, bool]]]):
302
331
  permission_mode = "skip" if skip_permissions else "normal"
303
332
  _save_claude_permission_mode(permission_mode)
304
333
 
334
+ # Get tracking mode
335
+ track_radio_set = self.query_one("#tracking-mode", RadioSet)
336
+ track_pressed = track_radio_set.pressed_button
337
+ no_track = track_pressed is not None and track_pressed.id == "track-notrack"
338
+ # Save the tracking mode for next time
339
+ tracking_mode = "notrack" if no_track else "track"
340
+ _save_claude_tracking_mode(tracking_mode)
341
+
305
342
  # Save the workspace path for next time
306
343
  _save_last_workspace(self._workspace_path)
307
344
 
308
345
  # Return the result
309
- self.dismiss((agent_type, self._workspace_path, skip_permissions))
346
+ self.dismiss((agent_type, self._workspace_path, skip_permissions, no_track))
@@ -45,6 +45,7 @@ OPT_TRANSCRIPT_PATH = "@aline_transcript_path"
45
45
  OPT_CONTEXT_ID = "@aline_context_id"
46
46
  OPT_ATTENTION = "@aline_attention"
47
47
  OPT_CREATED_AT = "@aline_created_at"
48
+ OPT_NO_TRACK = "@aline_no_track"
48
49
 
49
50
 
50
51
  @dataclass(frozen=True)
@@ -60,6 +61,7 @@ class InnerWindow:
60
61
  context_id: str | None = None
61
62
  attention: str | None = None # "permission_request", "stop", or None
62
63
  created_at: float | None = None # Unix timestamp when window was created
64
+ no_track: bool = False # Whether tracking is disabled for this terminal
63
65
 
64
66
 
65
67
  def tmux_available() -> bool:
@@ -604,6 +606,8 @@ def list_inner_windows() -> list[InnerWindow]:
604
606
  + OPT_ATTENTION
605
607
  + "}\t#{"
606
608
  + OPT_CREATED_AT
609
+ + "}\t#{"
610
+ + OPT_NO_TRACK
607
611
  + "}",
608
612
  ],
609
613
  capture=True,
@@ -632,6 +636,8 @@ def list_inner_windows() -> list[InnerWindow]:
632
636
  created_at = float(created_at_str)
633
637
  except ValueError:
634
638
  pass
639
+ no_track_str = parts[11] if len(parts) > 11 and parts[11] else None
640
+ no_track = no_track_str == "1"
635
641
 
636
642
  if terminal_id:
637
643
  persisted = state.get(terminal_id) or {}
@@ -663,6 +669,7 @@ def list_inner_windows() -> list[InnerWindow]:
663
669
  context_id=context_id,
664
670
  attention=attention,
665
671
  created_at=created_at,
672
+ no_track=no_track,
666
673
  )
667
674
  )
668
675
  # Sort by creation time (newest first). Windows without created_at go to the bottom.
@@ -758,6 +765,16 @@ def select_inner_window(window_id: str) -> bool:
758
765
  return _run_inner_tmux(["select-window", "-t", window_id]).returncode == 0
759
766
 
760
767
 
768
+ def focus_right_pane() -> bool:
769
+ """Focus the right pane (terminal area) in the outer tmux layout."""
770
+ return (
771
+ _run_outer_tmux(
772
+ ["select-pane", "-t", f"{OUTER_SESSION}:{OUTER_WINDOW}.1"]
773
+ ).returncode
774
+ == 0
775
+ )
776
+
777
+
761
778
  def clear_attention(window_id: str) -> bool:
762
779
  """Clear the attention state for a window (e.g., after user acknowledges permission request)."""
763
780
  if not ensure_inner_session():
@@ -888,6 +888,9 @@ class TerminalPanel(Container, can_focus=True):
888
888
  detail_line = (
889
889
  f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
890
890
  )
891
+ # Show no-track indicator
892
+ if w.metadata.get("no_track") == "1":
893
+ detail_line = f"{detail_line} [NT]"
891
894
  details.append(detail_line, style="dim not bold")
892
895
  return details
893
896
 
@@ -919,6 +922,9 @@ class TerminalPanel(Container, can_focus=True):
919
922
  detail_line = (
920
923
  f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
921
924
  )
925
+ # Show no-track indicator
926
+ if w.no_track:
927
+ detail_line = f"{detail_line} [NT]"
922
928
  details.append(detail_line, style="dim not bold")
923
929
  return details
924
930
 
@@ -944,24 +950,26 @@ class TerminalPanel(Container, can_focus=True):
944
950
  """Wrap a command to run in a specific directory."""
945
951
  return f"cd {shlex.quote(directory)} && {command}"
946
952
 
947
- def _on_create_agent_result(self, result: tuple[str, str, bool] | None) -> None:
953
+ def _on_create_agent_result(self, result: tuple[str, str, bool, bool] | None) -> None:
948
954
  """Handle the result from CreateAgentScreen modal."""
949
955
  if result is None:
950
956
  return
951
957
 
952
- agent_type, workspace, skip_permissions = result
958
+ agent_type, workspace, skip_permissions, no_track = result
953
959
  self.run_worker(
954
- self._create_agent(agent_type, workspace, skip_permissions=skip_permissions),
960
+ self._create_agent(
961
+ agent_type, workspace, skip_permissions=skip_permissions, no_track=no_track
962
+ ),
955
963
  group="terminal-panel-create",
956
964
  exclusive=True,
957
965
  )
958
966
 
959
967
  async def _create_agent(
960
- self, agent_type: str, workspace: str, *, skip_permissions: bool = False
968
+ self, agent_type: str, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
961
969
  ) -> None:
962
970
  """Create a new agent terminal based on the selected type and workspace."""
963
971
  if agent_type == "claude":
964
- await self._create_claude_terminal(workspace, skip_permissions=skip_permissions)
972
+ await self._create_claude_terminal(workspace, skip_permissions=skip_permissions, no_track=no_track)
965
973
  elif agent_type == "codex":
966
974
  await self._create_codex_terminal(workspace)
967
975
  elif agent_type == "opencode":
@@ -971,16 +979,16 @@ class TerminalPanel(Container, can_focus=True):
971
979
  await self.refresh_data()
972
980
 
973
981
  async def _create_claude_terminal(
974
- self, workspace: str, *, skip_permissions: bool = False
982
+ self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
975
983
  ) -> None:
976
984
  """Create a new Claude terminal."""
977
985
  if self._is_native_mode():
978
- await self._create_claude_terminal_native(workspace, skip_permissions=skip_permissions)
986
+ await self._create_claude_terminal_native(workspace, skip_permissions=skip_permissions, no_track=no_track)
979
987
  else:
980
- await self._create_claude_terminal_tmux(workspace, skip_permissions=skip_permissions)
988
+ await self._create_claude_terminal_tmux(workspace, skip_permissions=skip_permissions, no_track=no_track)
981
989
 
982
990
  async def _create_claude_terminal_native(
983
- self, workspace: str, *, skip_permissions: bool = False
991
+ self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
984
992
  ) -> None:
985
993
  """Create a new Claude terminal using native backend."""
986
994
  backend = await self._ensure_native_backend()
@@ -1000,6 +1008,8 @@ class TerminalPanel(Container, can_focus=True):
1000
1008
  tmux_manager.ENV_TERMINAL_PROVIDER: "claude",
1001
1009
  tmux_manager.ENV_CONTEXT_ID: context_id,
1002
1010
  }
1011
+ if no_track:
1012
+ env["ALINE_NO_TRACK"] = "1"
1003
1013
 
1004
1014
  # Install hooks
1005
1015
  self._install_claude_hooks(workspace)
@@ -1024,7 +1034,7 @@ class TerminalPanel(Container, can_focus=True):
1024
1034
  )
1025
1035
 
1026
1036
  async def _create_claude_terminal_tmux(
1027
- self, workspace: str, *, skip_permissions: bool = False
1037
+ self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
1028
1038
  ) -> None:
1029
1039
  """Create a new Claude terminal using tmux backend."""
1030
1040
  terminal_id = tmux_manager.new_terminal_id()
@@ -1036,6 +1046,8 @@ class TerminalPanel(Container, can_focus=True):
1036
1046
  tmux_manager.ENV_INNER_SESSION: tmux_manager.INNER_SESSION,
1037
1047
  tmux_manager.ENV_CONTEXT_ID: context_id,
1038
1048
  }
1049
+ if no_track:
1050
+ env["ALINE_NO_TRACK"] = "1"
1039
1051
 
1040
1052
  # Install hooks
1041
1053
  self._install_claude_hooks(workspace)
@@ -1239,7 +1251,7 @@ class TerminalPanel(Container, can_focus=True):
1239
1251
  if self._is_native_mode():
1240
1252
  backend = await self._ensure_native_backend()
1241
1253
  if backend:
1242
- success = await backend.focus_tab(window_id, steal_focus=False)
1254
+ success = await backend.focus_tab(window_id, steal_focus=True)
1243
1255
  if not success:
1244
1256
  self.app.notify(
1245
1257
  "Failed to switch terminal", title="Terminal", severity="error"
@@ -1247,6 +1259,9 @@ class TerminalPanel(Container, can_focus=True):
1247
1259
  else:
1248
1260
  if not tmux_manager.select_inner_window(window_id):
1249
1261
  self.app.notify("Failed to switch terminal", title="Terminal", severity="error")
1262
+ else:
1263
+ # Move cursor focus to the right pane (terminal area)
1264
+ tmux_manager.focus_right_pane()
1250
1265
  # Clear attention when user clicks on terminal
1251
1266
  tmux_manager.clear_attention(window_id)
1252
1267
 
realign/db/sqlite_db.py CHANGED
@@ -442,6 +442,21 @@ class SQLiteDatabase(DatabaseInterface):
442
442
  )
443
443
  conn.commit()
444
444
 
445
+ def update_session_metadata_flag(self, session_id: str, key: str, value: Any) -> None:
446
+ """Update a single key in session metadata JSON."""
447
+ conn = self._get_connection()
448
+ row = conn.execute(
449
+ "SELECT metadata FROM sessions WHERE id = ?", (session_id,)
450
+ ).fetchone()
451
+ if row:
452
+ meta = json.loads(row[0] or "{}")
453
+ meta[key] = value
454
+ conn.execute(
455
+ "UPDATE sessions SET metadata = ?, updated_at = datetime('now') WHERE id = ?",
456
+ (json.dumps(meta), session_id),
457
+ )
458
+ conn.commit()
459
+
445
460
  def backfill_session_total_turns(self) -> int:
446
461
  """Backfill total_turns for all sessions from turns table (V10 migration).
447
462
 
@@ -957,6 +972,7 @@ class SQLiteDatabase(DatabaseInterface):
957
972
  skip_session_summary: bool = False,
958
973
  expected_turns: Optional[int] = None,
959
974
  skip_dedup: bool = False,
975
+ no_track: bool = False,
960
976
  ) -> str:
961
977
  session_id = session_file_path.stem
962
978
  dedupe_key = f"turn:{session_id}:{int(turn_number)}"
@@ -973,6 +989,8 @@ class SQLiteDatabase(DatabaseInterface):
973
989
  payload["expected_turns"] = int(expected_turns)
974
990
  if skip_dedup:
975
991
  payload["skip_dedup"] = True
992
+ if no_track:
993
+ payload["no_track"] = True
976
994
 
977
995
  # For append-only session formats (Claude/Codex/Gemini), a turn is immutable once completed.
978
996
  # Avoid re-running already-done turn jobs on repeated enqueue attempts.
@@ -97,9 +97,24 @@ def update_session_summary_now(db: SQLiteDatabase, session_id: str) -> bool:
97
97
  pass
98
98
  return True
99
99
 
100
+ # Check session metadata for no_track mode
101
+ is_no_track = False
100
102
  try:
101
- # Generate title and summary using LLM
102
- title, summary = _generate_session_summary_llm(turns)
103
+ session = db.get_session_by_id(session_id)
104
+ if session:
105
+ session_meta = getattr(session, "metadata", None) or {}
106
+ is_no_track = bool(session_meta.get("no_track", False))
107
+ except Exception:
108
+ pass
109
+
110
+ try:
111
+ # Skip LLM call for no-track mode
112
+ if is_no_track:
113
+ title, summary = "No Track", "No Track"
114
+ logger.info(f"No-track mode: skipping LLM for session summary {session_id}")
115
+ else:
116
+ # Generate title and summary using LLM
117
+ title, summary = _generate_session_summary_llm(turns)
103
118
 
104
119
  # Update database
105
120
  db.update_session_summary(session_id, title, summary)
realign/watcher_core.py CHANGED
@@ -368,6 +368,7 @@ class DialogueWatcher:
368
368
  session_id = signal_data.get("session_id", "")
369
369
  project_dir = signal_data.get("project_dir", "")
370
370
  transcript_path = signal_data.get("transcript_path", "")
371
+ no_track = bool(signal_data.get("no_track", False))
371
372
 
372
373
  logger.info(f"Stop signal received for session {session_id}")
373
374
  print(f"[Watcher] Stop signal received for {session_id}", file=sys.stderr)
@@ -402,6 +403,7 @@ class DialogueWatcher:
402
403
  workspace_path=project_path,
403
404
  turn_number=target_turn,
404
405
  session_type=self._detect_session_type(session_file),
406
+ no_track=no_track,
405
407
  )
406
408
  except Exception as e:
407
409
  logger.warning(
@@ -462,6 +464,7 @@ class DialogueWatcher:
462
464
  prompt = str(signal_data.get("prompt") or "")
463
465
  transcript_path = str(signal_data.get("transcript_path") or "")
464
466
  project_dir = str(signal_data.get("project_dir") or "")
467
+ no_track = bool(signal_data.get("no_track", False))
465
468
 
466
469
  session_file = None
467
470
  if transcript_path and Path(transcript_path).exists():
@@ -488,6 +491,7 @@ class DialogueWatcher:
488
491
  session_id,
489
492
  prompt,
490
493
  project_dir,
494
+ no_track,
491
495
  )
492
496
 
493
497
  signal_file.unlink(missing_ok=True)
@@ -1318,6 +1322,7 @@ class DialogueWatcher:
1318
1322
  debug_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
1319
1323
  skip_dedup: bool = False,
1320
1324
  skip_session_summary: bool = False,
1325
+ no_track: bool = False,
1321
1326
  ) -> bool:
1322
1327
  """
1323
1328
  Execute commit with DB-backed lease locking to prevent cross-process races.
@@ -1365,6 +1370,7 @@ class DialogueWatcher:
1365
1370
  debug_callback=debug_callback,
1366
1371
  skip_dedup=skip_dedup,
1367
1372
  skip_session_summary=skip_session_summary,
1373
+ no_track=no_track,
1368
1374
  )
1369
1375
  except Exception as e:
1370
1376
  print(f"[Watcher] Commit error: {e}", file=sys.stderr)
@@ -1381,6 +1387,7 @@ class DialogueWatcher:
1381
1387
  debug_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
1382
1388
  skip_dedup: bool = False,
1383
1389
  skip_session_summary: bool = False,
1390
+ no_track: bool = False,
1384
1391
  ) -> bool:
1385
1392
  """
1386
1393
  Perform the actual commit operation to SQLite database.
@@ -1443,7 +1450,7 @@ class DialogueWatcher:
1443
1450
  file_created = datetime.fromtimestamp(
1444
1451
  getattr(file_stat, "st_birthtime", file_stat.st_ctime)
1445
1452
  )
1446
- db.get_or_create_session(
1453
+ session = db.get_or_create_session(
1447
1454
  session_id=session_id,
1448
1455
  session_file_path=session_file,
1449
1456
  session_type=self._detect_session_type(session_file),
@@ -1451,6 +1458,19 @@ class DialogueWatcher:
1451
1458
  workspace_path=str(project_path) if project_path else None,
1452
1459
  )
1453
1460
 
1461
+ # Check no_track from parameter or existing session metadata (polling path)
1462
+ is_no_track = no_track
1463
+ if not is_no_track and session:
1464
+ session_meta = getattr(session, "metadata", None) or {}
1465
+ is_no_track = bool(session_meta.get("no_track", False))
1466
+
1467
+ # Store no_track flag in session metadata if applicable
1468
+ if is_no_track:
1469
+ try:
1470
+ db.update_session_metadata_flag(session_id, "no_track", True)
1471
+ except Exception:
1472
+ pass
1473
+
1454
1474
  takeover_attempt = False
1455
1475
  existing_turn = db.get_turn_by_number(session_id, turn_number)
1456
1476
  if existing_turn and not skip_dedup:
@@ -1517,14 +1537,19 @@ class DialogueWatcher:
1517
1537
  logger.debug(f"Failed to write processing placeholder: {e}")
1518
1538
 
1519
1539
  try:
1520
- # Generate LLM summary with fallback for errors
1521
- llm_result = self._generate_llm_summary(
1522
- session_file,
1523
- turn_number=turn_number,
1524
- turn_content=turn_content,
1525
- user_message=user_message,
1526
- debug_callback=debug_callback,
1527
- )
1540
+ # Skip LLM call for no-track mode
1541
+ if is_no_track:
1542
+ llm_result = ("No Track", None, "No Track", "no", "fine")
1543
+ logger.info(f"No-track mode: skipping LLM for {session_id} turn {turn_number}")
1544
+ else:
1545
+ # Generate LLM summary with fallback for errors
1546
+ llm_result = self._generate_llm_summary(
1547
+ session_file,
1548
+ turn_number=turn_number,
1549
+ turn_content=turn_content,
1550
+ user_message=user_message,
1551
+ debug_callback=debug_callback,
1552
+ )
1528
1553
 
1529
1554
  if not llm_result:
1530
1555
  # LLM summary failed, use error marker to continue commit
@@ -1774,6 +1799,7 @@ class DialogueWatcher:
1774
1799
  session_id: str,
1775
1800
  prompt: str,
1776
1801
  project_dir: str,
1802
+ no_track: bool = False,
1777
1803
  ) -> None:
1778
1804
  """Generate and store a temporary turn title for a newly submitted user prompt."""
1779
1805
  try:
@@ -1802,20 +1828,28 @@ class DialogueWatcher:
1802
1828
  if not user_message:
1803
1829
  user_message = str(group.get("user_message") or "")
1804
1830
 
1805
- turn_content = self._extract_turn_content_by_number(session_file, turn_number)
1806
- result = self._generate_llm_summary(
1807
- session_file,
1808
- turn_number=turn_number,
1809
- turn_content=turn_content,
1810
- user_message=user_message or None,
1811
- session_id=session_id or session_file.stem,
1812
- )
1813
- if not result:
1814
- return
1831
+ # Skip LLM call for no-track mode
1832
+ if no_track:
1833
+ title = "No Track"
1834
+ model_name = None
1835
+ description = "No Track"
1836
+ if_last_task = "no"
1837
+ satisfaction = "fine"
1838
+ else:
1839
+ turn_content = self._extract_turn_content_by_number(session_file, turn_number)
1840
+ result = self._generate_llm_summary(
1841
+ session_file,
1842
+ turn_number=turn_number,
1843
+ turn_content=turn_content,
1844
+ user_message=user_message or None,
1845
+ session_id=session_id or session_file.stem,
1846
+ )
1847
+ if not result:
1848
+ return
1815
1849
 
1816
- title, model_name, description, if_last_task, satisfaction = result
1817
- if not title:
1818
- return
1850
+ title, model_name, description, if_last_task, satisfaction = result
1851
+ if not title:
1852
+ return
1819
1853
 
1820
1854
  from .db import get_database
1821
1855
  from .db.base import TurnRecord
realign/worker_core.py CHANGED
@@ -186,6 +186,7 @@ class AlineWorker:
186
186
  expected_turns_raw = payload.get("expected_turns")
187
187
  expected_turns = int(expected_turns_raw) if expected_turns_raw is not None else None
188
188
  skip_dedup = bool(payload.get("skip_dedup") or False)
189
+ no_track = bool(payload.get("no_track") or False)
189
190
 
190
191
  if not session_id or turn_number <= 0 or not session_file_path:
191
192
  raise ValueError(f"Invalid turn_summary payload: {payload}")
@@ -212,6 +213,7 @@ class AlineWorker:
212
213
  quiet=True,
213
214
  skip_session_summary=skip_session_summary,
214
215
  skip_dedup=skip_dedup,
216
+ no_track=no_track,
215
217
  )
216
218
 
217
219
  if created: