aline-ai 0.6.2__py3-none-any.whl → 0.6.4__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.
Files changed (40) hide show
  1. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
  3. realign/__init__.py +1 -1
  4. realign/adapters/__init__.py +0 -3
  5. realign/adapters/codex.py +14 -9
  6. realign/cli.py +42 -236
  7. realign/codex_detector.py +72 -32
  8. realign/codex_home.py +85 -0
  9. realign/codex_terminal_linker.py +172 -0
  10. realign/commands/__init__.py +2 -2
  11. realign/commands/add.py +89 -9
  12. realign/commands/doctor.py +495 -0
  13. realign/commands/export_shares.py +154 -226
  14. realign/commands/init.py +66 -4
  15. realign/commands/watcher.py +30 -80
  16. realign/config.py +9 -46
  17. realign/dashboard/app.py +7 -11
  18. realign/dashboard/screens/event_detail.py +0 -3
  19. realign/dashboard/screens/session_detail.py +0 -1
  20. realign/dashboard/tmux_manager.py +129 -4
  21. realign/dashboard/widgets/config_panel.py +175 -241
  22. realign/dashboard/widgets/events_table.py +71 -128
  23. realign/dashboard/widgets/sessions_table.py +77 -136
  24. realign/dashboard/widgets/terminal_panel.py +349 -27
  25. realign/dashboard/widgets/watcher_panel.py +0 -2
  26. realign/db/sqlite_db.py +77 -2
  27. realign/events/event_summarizer.py +76 -35
  28. realign/events/session_summarizer.py +73 -32
  29. realign/hooks.py +334 -647
  30. realign/llm_client.py +201 -520
  31. realign/triggers/__init__.py +0 -2
  32. realign/triggers/next_turn_trigger.py +4 -5
  33. realign/triggers/registry.py +1 -4
  34. realign/watcher_core.py +53 -35
  35. realign/adapters/antigravity.py +0 -159
  36. realign/triggers/antigravity_trigger.py +0 -140
  37. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
  38. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
  39. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
  40. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/top_level.txt +0 -0
@@ -524,7 +524,6 @@ def watcher_status_command(verbose: bool = False) -> int:
524
524
  "claude": "Claude Code",
525
525
  "codex": "Codex",
526
526
  "gemini": "Gemini",
527
- "antigravity": "Antigravity",
528
527
  }
529
528
  source = source_map.get(session_type, session_type)
530
529
 
@@ -583,7 +582,6 @@ def watcher_status_command(verbose: bool = False) -> int:
583
582
  "claude": "Claude Code",
584
583
  "codex": "Codex",
585
584
  "gemini": "Gemini",
586
- "antigravity": "Antigravity",
587
585
  }
588
586
  source = source_map.get(session_type, session_type)
589
587
 
@@ -1069,7 +1067,6 @@ def _get_session_tracking_status_batch(
1069
1067
  "claude": registry.get_adapter("claude"),
1070
1068
  "codex": registry.get_adapter("codex"),
1071
1069
  "gemini": registry.get_adapter("gemini"),
1072
- "antigravity": registry.get_adapter("antigravity"),
1073
1070
  }
1074
1071
 
1075
1072
  def _infer_adapter_name(session_file: Path) -> str:
@@ -1081,9 +1078,6 @@ def _get_session_tracking_status_batch(
1081
1078
  return "codex"
1082
1079
  if ".gemini" in parts:
1083
1080
  return "gemini"
1084
- # Antigravity "sessions" may be directories with markdown artifacts.
1085
- if session_file.is_dir():
1086
- return "antigravity"
1087
1081
  return "unknown"
1088
1082
 
1089
1083
  session_infos = []
@@ -1258,7 +1252,7 @@ def _get_session_tracking_status(session_file: Path, config: ReAlignConfig, db=N
1258
1252
  - committed_turns: int
1259
1253
  - total_turns: int
1260
1254
  - session_id: str
1261
- - source: str (claude/codex/gemini/antigravity)
1255
+ - source: str (claude/codex/gemini)
1262
1256
  - project_name: str | None
1263
1257
  - last_activity: datetime
1264
1258
  - session_file: Path
@@ -1449,7 +1443,8 @@ def watcher_session_list_command(
1449
1443
  console.print("[yellow]No sessions discovered.[/yellow]")
1450
1444
  console.print("[dim]Sessions are discovered from:[/dim]")
1451
1445
  console.print("[dim] • Claude Code: ~/.claude/projects/[/dim]")
1452
- console.print("[dim] • Codex: ~/.codex/sessions/[/dim]")
1446
+ console.print("[dim] • Codex (legacy): ~/.codex/sessions/[/dim]")
1447
+ console.print("[dim] • Codex (isolated): ~/.aline/codex_homes/*/sessions/[/dim]")
1453
1448
  console.print("[dim] • Gemini: ~/.gemini/tmp/*/chats/[/dim]")
1454
1449
  console.print("[dim] • Imported shares: Database[/dim]")
1455
1450
  return 0
@@ -1653,16 +1648,9 @@ def watcher_session_list_command(
1653
1648
  ]
1654
1649
 
1655
1650
  if detect_turns:
1656
- # Handle Antigravity sessions using timestamps as turn counts
1657
1651
  total_turns = info.get("total_turns") or 0
1658
1652
  committed_turns = info.get("committed_turns") or 0
1659
- if info["source"] == "antigravity" or total_turns > 1000000000:
1660
- # Normalize display for timestamp-based tracking
1661
- norm_total = 1
1662
- norm_committed = 1 if committed_turns > 0 else 0
1663
- turns_str = f"{norm_committed}/{norm_total}"
1664
- else:
1665
- turns_str = f"{committed_turns}/{total_turns}"
1653
+ turns_str = f"{committed_turns}/{total_turns}"
1666
1654
  row_data.append(turns_str)
1667
1655
 
1668
1656
  row_data.extend(
@@ -2212,7 +2200,6 @@ def watcher_event_list_command(
2212
2200
  def watcher_event_revise_slack_command(
2213
2201
  input_json: dict,
2214
2202
  instruction: str,
2215
- provider: str = "auto",
2216
2203
  json_output: bool = False,
2217
2204
  ) -> int:
2218
2205
  """
@@ -2227,7 +2214,6 @@ def watcher_event_revise_slack_command(
2227
2214
  - slack_message: Current Slack message
2228
2215
  - password: Optional password
2229
2216
  instruction: User's revision instructions
2230
- provider: LLM provider (auto, claude, openai)
2231
2217
  json_output: If True, output JSON (same format as input)
2232
2218
 
2233
2219
  Returns:
@@ -2237,7 +2223,7 @@ def watcher_event_revise_slack_command(
2237
2223
 
2238
2224
  try:
2239
2225
  from ..db import get_database
2240
- from ..hooks import _invoke_llm, _extract_json_object
2226
+ from ..llm_client import call_llm_cloud
2241
2227
 
2242
2228
  # Extract data from input JSON
2243
2229
  event_id = input_json.get("event_id")
@@ -2258,24 +2244,14 @@ def watcher_event_revise_slack_command(
2258
2244
  console.print("[red]Error: Missing 'slack_message' in input JSON[/red]")
2259
2245
  return 1
2260
2246
 
2261
- # Load prompt template
2247
+ # Load optional custom prompt
2248
+ custom_prompt = None
2262
2249
  prompt_path = Path.home() / ".aline" / "prompts" / "slack_share_revise.md"
2263
- if not prompt_path.exists():
2264
- # Fallback to example file
2265
- prompt_path = Path.home() / ".aline" / "prompts" / "slack_share_revise.md.example"
2266
-
2267
- if not prompt_path.exists():
2268
- if not json_output:
2269
- console.print(f"[red]Prompt file not found: {prompt_path}[/red]")
2270
- console.print("[dim]Run 'aline init' to initialize prompts[/dim]")
2271
- return 1
2272
-
2273
2250
  try:
2274
- system_prompt = prompt_path.read_text(encoding="utf-8")
2275
- except Exception as e:
2276
- if not json_output:
2277
- console.print(f"[red]Failed to read prompt file: {e}[/red]")
2278
- return 1
2251
+ if prompt_path.exists():
2252
+ custom_prompt = prompt_path.read_text(encoding="utf-8").strip()
2253
+ except Exception:
2254
+ pass
2279
2255
 
2280
2256
  # Build event context from input JSON
2281
2257
  context_parts = []
@@ -2286,35 +2262,21 @@ def watcher_event_revise_slack_command(
2286
2262
 
2287
2263
  event_context = "\n".join(context_parts) if context_parts else "No additional context"
2288
2264
 
2289
- # Construct user prompt
2290
- user_prompt = f"""**Original Event Context:**
2291
- ```
2292
- {event_context}
2293
- ```
2294
-
2295
- **Previous Message:**
2296
- ```
2297
- {slack_message}
2298
- ```
2299
-
2300
- **Revision Request:**
2301
- ```
2302
- {instruction}
2303
- ```
2304
- """
2305
-
2306
- # Call LLM to revise the message
2265
+ # Call cloud LLM to revise the message
2307
2266
  if not json_output:
2308
2267
  console.print(f"→ Revising Slack message for event: [cyan]{event_title}[/cyan]")
2309
- logger.info(f"Calling LLM to revise Slack message for event {event_id}")
2268
+ logger.info(f"Calling cloud LLM to revise Slack message for event {event_id}")
2310
2269
 
2311
2270
  try:
2312
- model_name, response_text = _invoke_llm(
2313
- provider=provider,
2314
- system_prompt=system_prompt,
2315
- user_prompt=user_prompt,
2316
- purpose="revise_slack_message",
2317
- silent=True, # Always silent - suppress LLM provider messages
2271
+ model_name, result = call_llm_cloud(
2272
+ task="revise_slack_message",
2273
+ payload={
2274
+ "event_context": event_context,
2275
+ "current_message": slack_message,
2276
+ "revision_instruction": instruction,
2277
+ },
2278
+ custom_prompt=custom_prompt,
2279
+ silent=True,
2318
2280
  )
2319
2281
  except Exception as e:
2320
2282
  if not json_output:
@@ -2322,27 +2284,18 @@ def watcher_event_revise_slack_command(
2322
2284
  logger.error(f"LLM invocation failed: {e}", exc_info=True)
2323
2285
  return 1
2324
2286
 
2325
- if not response_text:
2287
+ if not result:
2326
2288
  if not json_output:
2327
2289
  console.print("[red]LLM did not return a response[/red]")
2328
2290
  logger.error("LLM returned empty response")
2329
2291
  return 1
2330
2292
 
2331
- # Parse JSON response to extract the revised message
2332
- try:
2333
- parsed = _extract_json_object(response_text)
2334
- if isinstance(parsed, dict) and "message" in parsed:
2335
- revised_message = parsed["message"]
2336
- else:
2337
- # Fallback: use the raw response if JSON parsing fails or no message field
2338
- revised_message = response_text
2339
- logger.warning(
2340
- "Response did not contain expected 'message' field, using raw response"
2341
- )
2342
- except Exception as e:
2343
- # Fallback: use the raw response if JSON parsing fails
2344
- revised_message = response_text
2345
- logger.warning(f"Failed to parse JSON response: {e}, using raw response")
2293
+ revised_message = result.get("message", "")
2294
+ if not revised_message:
2295
+ if not json_output:
2296
+ console.print("[red]LLM response missing 'message' field[/red]")
2297
+ logger.error("LLM response missing 'message' field")
2298
+ return 1
2346
2299
 
2347
2300
  # Update the event in the database
2348
2301
  db = get_database()
@@ -3201,8 +3154,7 @@ def watcher_session_show_command(
3201
3154
  "claude": "Claude Code",
3202
3155
  "codex": "Codex",
3203
3156
  "gemini": "Gemini",
3204
- "antigravity": "Antigravity",
3205
- }
3157
+ }
3206
3158
  source = source_map.get(info["source"], info["source"])
3207
3159
 
3208
3160
  # Build turns data
@@ -3301,7 +3253,6 @@ def watcher_session_show_command(
3301
3253
  "claude": "Claude Code",
3302
3254
  "codex": "Codex",
3303
3255
  "gemini": "Gemini",
3304
- "antigravity": "Antigravity",
3305
3256
  }
3306
3257
  source = source_map.get(info["source"], info["source"])
3307
3258
 
@@ -3662,7 +3613,6 @@ def watcher_session_delete_command(
3662
3613
  "claude": "Claude Code",
3663
3614
  "codex": "Codex",
3664
3615
  "gemini": "Gemini",
3665
- "antigravity": "Antigravity",
3666
3616
  }
3667
3617
  source = source_map.get(info.get("source", ""), info.get("source", "unknown"))
3668
3618
 
realign/config.py CHANGED
@@ -20,7 +20,6 @@ class ReAlignConfig:
20
20
  auto_detect_claude: bool = True # Enable Claude Code session auto-detection
21
21
  auto_detect_codex: bool = True # Enable Codex session auto-detection
22
22
  auto_detect_gemini: bool = True # Enable Gemini CLI session auto-detection
23
- auto_detect_antigravity: bool = False # Enable Antigravity IDE brain artifact monitoring
24
23
  mcp_auto_commit: bool = True # Enable watcher auto-commit after each user request completes
25
24
  enable_temp_turn_titles: bool = True # Generate temporary turn titles on user prompt submit
26
25
  share_backend_url: str = (
@@ -34,16 +33,9 @@ class ReAlignConfig:
34
33
  # Session catch-up settings
35
34
  max_catchup_sessions: int = 3 # Max sessions to auto-import on watcher startup
36
35
 
37
- # LLM API Keys
38
- anthropic_api_key: Optional[str] = None # Anthropic API key (set in config, not environment)
39
- openai_api_key: Optional[str] = None # OpenAI API key (set in config, not environment)
40
-
41
- # LLM Model Configuration
42
- llm_anthropic_model: str = "claude-3-5-haiku-20241022" # Claude model to use
43
- llm_openai_model: str = "gpt-4o-mini" # OpenAI model to use
44
- llm_openai_use_responses: bool = False # Use OpenAI Responses API for reasoning models
45
- llm_max_tokens: int = 1000 # Default max tokens
46
- llm_temperature: float = 0.0 # Default temperature (0.0 = deterministic)
36
+ # Terminal auto-close settings
37
+ auto_close_stale_terminals: bool = False # Auto-close terminals inactive for 24+ hours
38
+ stale_terminal_hours: int = 24 # Hours of inactivity before auto-closing
47
39
 
48
40
  @classmethod
49
41
  def load(cls, config_path: Optional[Path] = None) -> "ReAlignConfig":
@@ -87,38 +79,29 @@ class ReAlignConfig:
87
79
  "auto_detect_claude": os.getenv("REALIGN_AUTO_DETECT_CLAUDE"),
88
80
  "auto_detect_codex": os.getenv("REALIGN_AUTO_DETECT_CODEX"),
89
81
  "auto_detect_gemini": os.getenv("REALIGN_AUTO_DETECT_GEMINI"),
90
- "auto_detect_antigravity": os.getenv("REALIGN_AUTO_DETECT_ANTIGRAVITY"),
91
82
  "mcp_auto_commit": os.getenv("REALIGN_MCP_AUTO_COMMIT"),
92
83
  "enable_temp_turn_titles": os.getenv("REALIGN_ENABLE_TEMP_TURN_TITLES"),
93
84
  "share_backend_url": os.getenv("REALIGN_SHARE_BACKEND_URL"),
94
85
  "user_name": os.getenv("REALIGN_USER_NAME"),
95
86
  "uid": os.getenv("REALIGN_UID"),
96
87
  "max_catchup_sessions": os.getenv("REALIGN_MAX_CATCHUP_SESSIONS"),
97
- "anthropic_api_key": os.getenv("REALIGN_ANTHROPIC_API_KEY"),
98
- "openai_api_key": os.getenv("REALIGN_OPENAI_API_KEY"),
99
- "llm_anthropic_model": os.getenv("REALIGN_ANTHROPIC_MODEL"),
100
- "llm_openai_model": os.getenv("REALIGN_OPENAI_MODEL"),
101
- "llm_openai_use_responses": os.getenv("REALIGN_OPENAI_USE_RESPONSES"),
102
- "llm_max_tokens": os.getenv("REALIGN_LLM_MAX_TOKENS"),
103
- "llm_temperature": os.getenv("REALIGN_LLM_TEMPERATURE"),
88
+ "auto_close_stale_terminals": os.getenv("REALIGN_AUTO_CLOSE_STALE_TERMINALS"),
89
+ "stale_terminal_hours": os.getenv("REALIGN_STALE_TERMINAL_HOURS"),
104
90
  }
105
91
 
106
92
  for key, value in env_overrides.items():
107
93
  if value is not None:
108
- if key in ["summary_max_chars", "max_catchup_sessions", "llm_max_tokens"]:
94
+ if key in ["summary_max_chars", "max_catchup_sessions", "stale_terminal_hours"]:
109
95
  config_dict[key] = int(value)
110
- elif key in ["llm_temperature"]:
111
- config_dict[key] = float(value)
112
96
  elif key in [
113
97
  "redact_on_match",
114
98
  "use_LLM",
115
99
  "auto_detect_claude",
116
100
  "auto_detect_codex",
117
101
  "auto_detect_gemini",
118
- "auto_detect_antigravity",
119
102
  "mcp_auto_commit",
120
103
  "enable_temp_turn_titles",
121
- "llm_openai_use_responses",
104
+ "auto_close_stale_terminals",
122
105
  ]:
123
106
  config_dict[key] = value.lower() in ("true", "1", "yes")
124
107
  else:
@@ -150,20 +133,14 @@ class ReAlignConfig:
150
133
  "auto_detect_claude": self.auto_detect_claude,
151
134
  "auto_detect_codex": self.auto_detect_codex,
152
135
  "auto_detect_gemini": self.auto_detect_gemini,
153
- "auto_detect_antigravity": self.auto_detect_antigravity,
154
136
  "mcp_auto_commit": self.mcp_auto_commit,
155
137
  "enable_temp_turn_titles": self.enable_temp_turn_titles,
156
138
  "share_backend_url": self.share_backend_url,
157
139
  "user_name": self.user_name,
158
140
  "uid": self.uid,
159
141
  "max_catchup_sessions": self.max_catchup_sessions,
160
- "anthropic_api_key": self.anthropic_api_key,
161
- "openai_api_key": self.openai_api_key,
162
- "llm_anthropic_model": self.llm_anthropic_model,
163
- "llm_openai_model": self.llm_openai_model,
164
- "llm_openai_use_responses": self.llm_openai_use_responses,
165
- "llm_max_tokens": self.llm_max_tokens,
166
- "llm_temperature": self.llm_temperature,
142
+ "auto_close_stale_terminals": self.auto_close_stale_terminals,
143
+ "stale_terminal_hours": self.stale_terminal_hours,
167
144
  }
168
145
 
169
146
  with open(config_path, "w", encoding="utf-8") as f:
@@ -200,7 +177,6 @@ use_LLM: true # Whether to use a cloud LLM to generate summar
200
177
  llm_provider: "auto" # LLM provider: "auto" (try Claude then OpenAI), "claude", or "openai"
201
178
  auto_detect_claude: true # Automatically detect Claude Code session directory (~/.claude/projects/)
202
179
  auto_detect_codex: true # Automatically detect Codex session files (~/.codex/sessions/)
203
- auto_detect_antigravity: false # Automatically detect Antigravity IDE brain artifacts (~/.gemini/antigravity/brain/)
204
180
  mcp_auto_commit: true # Enable watcher to auto-commit after each user request completes
205
181
  enable_temp_turn_titles: true # Generate temporary turn titles on user prompt submit
206
182
  share_backend_url: "https://realign-server.vercel.app" # Backend URL for interactive share export
@@ -211,19 +187,6 @@ max_catchup_sessions: 3 # Max sessions to auto-import on watcher
211
187
  # Use 'aline watcher session list' to see all sessions
212
188
  # Use 'aline watcher session import <id>' to import specific sessions
213
189
 
214
- # LLM API Keys (configured in this file only, NOT from environment variables):
215
- # anthropic_api_key: "your-anthropic-api-key" # For Claude (Anthropic)
216
- # openai_api_key: "your-openai-api-key" # For OpenAI (GPT)
217
- # Note: API keys are read ONLY from this config file, not from system environment variables
218
- # Alternative: Use REALIGN_ANTHROPIC_API_KEY or REALIGN_OPENAI_API_KEY env vars to override
219
-
220
- # LLM Model Configuration:
221
- llm_anthropic_model: "claude-3-5-haiku-20241022" # Claude model to use
222
- llm_openai_model: "gpt-4o-mini" # OpenAI model to use
223
- llm_openai_use_responses: false # Use OpenAI Responses API for reasoning models (GPT-5+)
224
- llm_max_tokens: 1000 # Default max tokens for LLM responses
225
- llm_temperature: 0.0 # Default temperature (0.0 = deterministic, 1.0 = creative)
226
-
227
190
  # Secret Detection & Redaction:
228
191
  # ReAlign can use detect-secrets to automatically scan for and redact:
229
192
  # - API keys, tokens, passwords
realign/dashboard/app.py CHANGED
@@ -242,10 +242,7 @@ class AlineDashboard(App):
242
242
  self.query_one(WatcherPanel).action_next_page()
243
243
  elif active_tab_id == "worker":
244
244
  self.query_one(WorkerPanel).action_next_page()
245
- elif active_tab_id == "sessions":
246
- self.query_one(SessionsTable).action_next_page()
247
- elif active_tab_id == "events":
248
- self.query_one(EventsTable).action_next_page()
245
+ # sessions and events tabs use scrolling instead of pagination
249
246
 
250
247
  def action_page_prev(self) -> None:
251
248
  """Go to previous page in current panel."""
@@ -256,10 +253,7 @@ class AlineDashboard(App):
256
253
  self.query_one(WatcherPanel).action_prev_page()
257
254
  elif active_tab_id == "worker":
258
255
  self.query_one(WorkerPanel).action_prev_page()
259
- elif active_tab_id == "sessions":
260
- self.query_one(SessionsTable).action_prev_page()
261
- elif active_tab_id == "events":
262
- self.query_one(EventsTable).action_prev_page()
256
+ # sessions and events tabs use scrolling instead of pagination
263
257
 
264
258
  def action_switch_view(self) -> None:
265
259
  """Switch view in current panel (if supported)."""
@@ -329,20 +323,22 @@ class AlineDashboard(App):
329
323
  self.push_screen(ShareImportScreen())
330
324
 
331
325
  async def action_load_context(self) -> None:
332
- """Load selected sessions/events into the active Claude terminal context."""
326
+ """Load selected sessions/events into the active terminal context (Claude/Codex)."""
333
327
  tabbed_content = self.query_one(TabbedContent)
334
328
  active_tab_id = tabbed_content.active
335
329
 
336
330
  try:
337
331
  from . import tmux_manager
338
332
 
339
- context_id = tmux_manager.get_active_claude_context_id()
333
+ context_id = tmux_manager.get_active_context_id(
334
+ allowed_providers={"claude", "codex"}
335
+ )
340
336
  except Exception:
341
337
  context_id = None
342
338
 
343
339
  if not context_id:
344
340
  self.notify(
345
- "No active Claude context found. Use the Terminal tab and select a 'cc' terminal (New cc).",
341
+ "No active context found. Use the Terminal tab and select a Claude ('cc') or Codex terminal.",
346
342
  title="Load Context",
347
343
  severity="warning",
348
344
  timeout=4,
@@ -364,7 +364,6 @@ class EventDetailScreen(ModalScreen):
364
364
  "claude": "Claude",
365
365
  "codex": "Codex",
366
366
  "gemini": "Gemini",
367
- "antigravity": "Antigravity",
368
367
  }
369
368
  source = source_map.get(session_type, session_type)
370
369
  project = workspace.split("/")[-1] if workspace else "-"
@@ -398,7 +397,6 @@ class EventDetailScreen(ModalScreen):
398
397
  "claude": "Claude",
399
398
  "codex": "Codex",
400
399
  "gemini": "Gemini",
401
- "antigravity": "Antigravity",
402
400
  }
403
401
  source = source_map.get(session_type, session_type)
404
402
  project = str(workspace).split("/")[-1] if workspace else "-"
@@ -533,7 +531,6 @@ class EventDetailScreen(ModalScreen):
533
531
  "claude": "Claude",
534
532
  "codex": "Codex",
535
533
  "gemini": "Gemini",
536
- "antigravity": "Antigravity",
537
534
  }
538
535
  source = source_map.get(
539
536
  record_type or "", record_type or session.get("source") or "unknown"
@@ -171,7 +171,6 @@ class SessionDetailScreen(ModalScreen):
171
171
  "claude": "Claude",
172
172
  "codex": "Codex",
173
173
  "gemini": "Gemini",
174
- "antigravity": "Antigravity",
175
174
  }
176
175
  source = source_map.get(session_type or "", session_type or "unknown")
177
176
 
@@ -516,9 +516,21 @@ def ensure_inner_session() -> bool:
516
516
  return False
517
517
 
518
518
  if _run_inner_tmux(["has-session", "-t", INNER_SESSION]).returncode != 0:
519
- if _run_inner_tmux(["new-session", "-d", "-s", INNER_SESSION]).returncode != 0:
519
+ # Create a stable "home" window so user-created terminals can use names like "zsh"
520
+ # without always becoming "zsh-2".
521
+ if (
522
+ _run_inner_tmux(["new-session", "-d", "-s", INNER_SESSION, "-n", "home"]).returncode
523
+ != 0
524
+ ):
520
525
  return False
521
526
 
527
+ # Ensure the default/home window stays named "home" (tmux auto-rename would otherwise
528
+ # change it to "zsh"/"opencode" depending on the last foreground command).
529
+ try:
530
+ _ensure_inner_home_window()
531
+ except Exception:
532
+ pass
533
+
522
534
  # Dedicated inner server; safe to enable mouse globally there.
523
535
  _run_inner_tmux(["set-option", "-g", "mouse", "on"])
524
536
 
@@ -537,6 +549,101 @@ def ensure_inner_session() -> bool:
537
549
  return True
538
550
 
539
551
 
552
+ def _ensure_inner_home_window() -> None:
553
+ """Ensure the inner session has a reserved, non-renaming 'home' window (best-effort)."""
554
+ if _run_inner_tmux(["has-session", "-t", INNER_SESSION]).returncode != 0:
555
+ return
556
+
557
+ out = (
558
+ _run_inner_tmux(
559
+ [
560
+ "list-windows",
561
+ "-t",
562
+ INNER_SESSION,
563
+ "-F",
564
+ "#{window_id}\t#{window_index}\t#{window_name}\t#{"
565
+ + OPT_TERMINAL_ID
566
+ + "}\t#{"
567
+ + OPT_PROVIDER
568
+ + "}\t#{"
569
+ + OPT_SESSION_TYPE
570
+ + "}\t#{"
571
+ + OPT_CONTEXT_ID
572
+ + "}\t#{"
573
+ + OPT_CREATED_AT
574
+ + "}\t#{"
575
+ + OPT_NO_TRACK
576
+ + "}",
577
+ ],
578
+ capture=True,
579
+ ).stdout
580
+ or ""
581
+ )
582
+
583
+ candidates: list[tuple[str, int, str, str, str, str, str, str, str]] = []
584
+ for line in _parse_lines(out):
585
+ parts = (line.split("\t", 8) + [""] * 9)[:9]
586
+ window_id = parts[0]
587
+ try:
588
+ window_index = int(parts[1])
589
+ except Exception:
590
+ window_index = 9999
591
+ window_name = parts[2]
592
+ terminal_id = parts[3]
593
+ provider = parts[4]
594
+ session_type = parts[5]
595
+ context_id = parts[6]
596
+ created_at = parts[7]
597
+ no_track = parts[8]
598
+
599
+ # Pick an unmanaged window (the default one created by `new-session`) as "home".
600
+ unmanaged = (
601
+ not (terminal_id or "").strip()
602
+ and not (provider or "").strip()
603
+ and not (session_type or "").strip()
604
+ and not (context_id or "").strip()
605
+ and not (created_at or "").strip()
606
+ )
607
+ if unmanaged:
608
+ candidates.append(
609
+ (
610
+ window_id,
611
+ window_index,
612
+ window_name,
613
+ terminal_id,
614
+ provider,
615
+ session_type,
616
+ context_id,
617
+ created_at,
618
+ no_track,
619
+ )
620
+ )
621
+
622
+ if not candidates:
623
+ return
624
+
625
+ # Prefer the first window (index 0) if present.
626
+ candidates.sort(key=lambda t: t[1])
627
+ window_id = candidates[0][0]
628
+
629
+ # Rename to "home" and prevent tmux auto-renaming it based on foreground command.
630
+ _run_inner_tmux(["rename-window", "-t", window_id, "home"])
631
+ _run_inner_tmux(["set-option", "-w", "-t", window_id, "automatic-rename", "off"])
632
+ _run_inner_tmux(["set-option", "-w", "-t", window_id, "allow-rename", "off"])
633
+
634
+ # Mark as internal/no-track so UI can hide it.
635
+ try:
636
+ set_inner_window_options(
637
+ window_id,
638
+ {
639
+ OPT_NO_TRACK: "1",
640
+ OPT_CREATED_AT: str(time.time()),
641
+ },
642
+ )
643
+ except Exception:
644
+ pass
645
+
646
+
540
647
  def ensure_right_pane(width_percent: int = 50) -> bool:
541
648
  """Create the right-side pane (terminal area) if it doesn't exist.
542
649
 
@@ -701,6 +808,7 @@ def create_inner_window(
701
808
  terminal_id: str | None = None,
702
809
  provider: str | None = None,
703
810
  context_id: str | None = None,
811
+ no_track: bool = False,
704
812
  ) -> InnerWindow | None:
705
813
  if not ensure_right_pane():
706
814
  return None
@@ -744,6 +852,10 @@ def create_inner_window(
744
852
  opts.setdefault(OPT_SESSION_TYPE, "")
745
853
  opts.setdefault(OPT_SESSION_ID, "")
746
854
  opts.setdefault(OPT_TRANSCRIPT_PATH, "")
855
+ if no_track:
856
+ opts[OPT_NO_TRACK] = "1"
857
+ else:
858
+ opts.setdefault(OPT_NO_TRACK, "")
747
859
  set_inner_window_options(window_id, opts)
748
860
 
749
861
  _run_inner_tmux(["select-window", "-t", window_id])
@@ -784,6 +896,16 @@ def clear_attention(window_id: str) -> bool:
784
896
 
785
897
  def get_active_claude_context_id() -> str | None:
786
898
  """Return the active inner tmux window's Claude ALINE_CONTEXT_ID (if any)."""
899
+ return get_active_context_id(allowed_providers={"claude"})
900
+
901
+
902
+ def get_active_codex_context_id() -> str | None:
903
+ """Return the active inner tmux window's Codex ALINE_CONTEXT_ID (if any)."""
904
+ return get_active_context_id(allowed_providers={"codex"})
905
+
906
+
907
+ def get_active_context_id(*, allowed_providers: set[str] | None = None) -> str | None:
908
+ """Return the active inner tmux window's ALINE_CONTEXT_ID (optionally filtered by provider)."""
787
909
  try:
788
910
  windows = list_inner_windows()
789
911
  except Exception:
@@ -793,9 +915,12 @@ def get_active_claude_context_id() -> str | None:
793
915
  if active is None:
794
916
  return None
795
917
 
796
- is_claude = (active.provider == "claude") or (active.session_type == "claude")
797
- if not is_claude:
798
- return None
918
+ if allowed_providers is not None:
919
+ allowed = {str(p).strip() for p in allowed_providers if str(p).strip()}
920
+ provider = (active.provider or "").strip()
921
+ session_type = (active.session_type or "").strip()
922
+ if provider not in allowed and session_type not in allowed:
923
+ return None
799
924
 
800
925
  context_id = (active.context_id or "").strip()
801
926
  return context_id or None