aline-ai 0.6.2__py3-none-any.whl → 0.6.3__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.
@@ -80,23 +80,16 @@ class SessionsTable(Container):
80
80
  }
81
81
 
82
82
  SessionsTable .table-container {
83
- height: auto;
83
+ height: 1fr;
84
84
  overflow-x: auto;
85
- overflow-y: hidden;
85
+ overflow-y: auto;
86
86
  }
87
87
 
88
88
  SessionsTable DataTable {
89
89
  height: auto;
90
90
  max-height: 100%;
91
91
  overflow-x: auto;
92
- overflow-y: hidden;
93
- }
94
-
95
- SessionsTable .pagination-info {
96
- height: 1;
97
- margin-top: 1;
98
- color: $text-muted;
99
- text-align: center;
92
+ overflow-y: auto;
100
93
  }
101
94
 
102
95
  SessionsTable .stats-info {
@@ -107,9 +100,9 @@ class SessionsTable(Container):
107
100
  """
108
101
 
109
102
  # Reactive properties
110
- current_page: reactive[int] = reactive(1)
111
- rows_per_page: reactive[int] = reactive(10)
112
103
  wrap_mode: reactive[bool] = reactive(False)
104
+ # Maximum sessions to load (for scrollable list)
105
+ MAX_SESSIONS: int = 500
113
106
 
114
107
  def __init__(self) -> None:
115
108
  super().__init__()
@@ -123,8 +116,9 @@ class SessionsTable(Container):
123
116
  self._refresh_worker: Optional[Worker] = None
124
117
  self._share_export_worker: Optional[Worker] = None
125
118
  self._refresh_timer = None
126
- self._active_refresh_snapshot: Optional[tuple[int, int]] = None
127
- self._pending_refresh_snapshot: Optional[tuple[int, int]] = None
119
+ self._is_refreshing: bool = False
120
+ self._pending_refresh: bool = False
121
+ self._saved_cursor_session_id: Optional[str] = None
128
122
 
129
123
  def compose(self) -> ComposeResult:
130
124
  """Compose the sessions table layout."""
@@ -144,7 +138,6 @@ class SessionsTable(Container):
144
138
  yield Static(id="section-header", classes="section-header")
145
139
  with Container(classes="table-container"):
146
140
  yield SessionsListTable(id="sessions-table")
147
- yield Static(id="pagination-info", classes="pagination-info")
148
141
  yield Static(id="stats-info", classes="stats-info")
149
142
 
150
143
  def on_mount(self) -> None:
@@ -155,22 +148,19 @@ class SessionsTable(Container):
155
148
  table.owner = self
156
149
 
157
150
  self._setup_table_columns(table)
158
-
159
- # Calculate initial rows per page
160
- self._calculate_rows_per_page()
161
151
  logger.debug("SessionsTable.on_mount() completed")
162
152
  except Exception as e:
163
153
  logger.error(f"SessionsTable.on_mount() failed: {e}\n{traceback.format_exc()}")
164
154
  raise
165
155
 
166
156
  def on_resize(self) -> None:
167
- """Handle window resize to adjust rows per page."""
168
- self._sync_to_available_height()
157
+ """Handle window resize."""
158
+ pass # No longer need to recalculate pagination
169
159
 
170
160
  def on_show(self) -> None:
171
- """Re-sync pagination when the tab becomes visible."""
161
+ """Refresh data when the tab becomes visible."""
172
162
  if self._refresh_timer is None:
173
- self._refresh_timer = self.set_interval(5.0, self._on_refresh_timer)
163
+ self._refresh_timer = self.set_interval(60.0, self._on_refresh_timer)
174
164
  else:
175
165
  try:
176
166
  self._refresh_timer.resume()
@@ -188,9 +178,7 @@ class SessionsTable(Container):
188
178
  pass
189
179
 
190
180
  def _on_became_visible(self) -> None:
191
- self._sync_to_available_height()
192
181
  self._load_sessions()
193
- self._update_display()
194
182
  try:
195
183
  self.query_one("#sessions-table", DataTable).focus()
196
184
  except Exception:
@@ -229,19 +217,6 @@ class SessionsTable(Container):
229
217
  table.add_column("Last Activity", key="last_activity", width=12)
230
218
  table.cursor_type = "row"
231
219
 
232
- def _sync_to_available_height(self) -> None:
233
- """Recalculate rows per page and reload if the page size changed."""
234
- old_rows_per_page = self.rows_per_page
235
- self._calculate_rows_per_page()
236
-
237
- if self.rows_per_page != old_rows_per_page:
238
- total_pages = self._get_total_pages()
239
- if self.current_page > total_pages:
240
- self.current_page = total_pages
241
- self._load_sessions()
242
-
243
- self._update_display()
244
-
245
220
  def get_selected_session_ids(self) -> list[str]:
246
221
  return sorted(self._selected_session_ids)
247
222
 
@@ -423,47 +398,6 @@ class SessionsTable(Container):
423
398
  self._start_share_export()
424
399
  return
425
400
 
426
- def _calculate_rows_per_page(self) -> None:
427
- """Calculate rows per page based on available height."""
428
- try:
429
- panel_height = self.size.height
430
-
431
- # Calculate exact space needed:
432
- # - Summary section: ~2 lines content + 2 border + 2 padding + 1 margin = 7
433
- # - Section header: 1 line + 1 margin = 2
434
- # - Table header row: 1
435
- # - Pagination info: 1 line + 1 margin = 2
436
- # - Panel padding: 2 (top + bottom)
437
- # - Extra buffer: 3
438
-
439
- fixed_height = 7 + 2 + 1 + 2 + 2 + 3 # = 17
440
-
441
- available_for_rows = panel_height - fixed_height
442
- rows = max(available_for_rows, 3) # At least 3 rows
443
-
444
- self.rows_per_page = rows
445
- except Exception:
446
- self.rows_per_page = 10
447
-
448
- def _get_total_pages(self) -> int:
449
- """Calculate total pages."""
450
- if self._total_sessions == 0:
451
- return 1
452
- return (self._total_sessions + self.rows_per_page - 1) // self.rows_per_page
453
-
454
- def action_next_page(self) -> None:
455
- """Go to next page."""
456
- total_pages = self._get_total_pages()
457
- if self.current_page < total_pages:
458
- self.current_page += 1
459
- self._load_sessions()
460
-
461
- def action_prev_page(self) -> None:
462
- """Go to previous page."""
463
- if self.current_page > 1:
464
- self.current_page -= 1
465
- self._load_sessions()
466
-
467
401
  def _on_refresh_timer(self) -> None:
468
402
  self.refresh_data(force=False)
469
403
 
@@ -472,26 +406,37 @@ class SessionsTable(Container):
472
406
  if not self.display:
473
407
  return
474
408
 
475
- snapshot = (int(self.current_page), int(self.rows_per_page))
476
409
  if self._refresh_worker is not None and self._refresh_worker.state in (
477
410
  WorkerState.PENDING,
478
411
  WorkerState.RUNNING,
479
412
  ):
480
413
  if force:
481
- self._pending_refresh_snapshot = snapshot
414
+ self._pending_refresh = True
482
415
  return
483
416
 
484
- if force and self._pending_refresh_snapshot is not None:
485
- snapshot = self._pending_refresh_snapshot
486
- self._pending_refresh_snapshot = None
417
+ # Save current cursor position before refresh
418
+ self._save_cursor_position()
487
419
 
488
420
  def work() -> dict:
489
- return self._collect_snapshot(*snapshot)
421
+ return self._collect_all_sessions()
490
422
 
491
- self._active_refresh_snapshot = snapshot
492
- self._pending_refresh_snapshot = None
423
+ self._is_refreshing = True
424
+ self._pending_refresh = False
493
425
  self._refresh_worker = self.run_worker(work, thread=True, exit_on_error=False)
494
426
 
427
+ def _save_cursor_position(self) -> None:
428
+ """Save the current cursor session ID to restore after refresh."""
429
+ try:
430
+ table = self.query_one("#sessions-table", DataTable)
431
+ if table.row_count > 0:
432
+ self._saved_cursor_session_id = str(
433
+ table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
434
+ )
435
+ else:
436
+ self._saved_cursor_session_id = None
437
+ except Exception:
438
+ self._saved_cursor_session_id = None
439
+
495
440
  def _load_sessions(self) -> None:
496
441
  """Compatibility hook (tests stub this); default triggers async refresh."""
497
442
  if not self.is_mounted:
@@ -513,35 +458,34 @@ class SessionsTable(Container):
513
458
 
514
459
  if event.state == WorkerState.ERROR:
515
460
  result = {
516
- "snapshot": self._active_refresh_snapshot,
517
461
  "total_sessions": 0,
518
462
  "stats": {},
519
463
  "sessions": [],
520
464
  }
465
+ self._is_refreshing = False
521
466
  elif event.state != WorkerState.SUCCESS:
522
467
  return
523
468
  else:
524
469
  result = self._refresh_worker.result or {}
470
+ self._is_refreshing = False
525
471
 
526
- snapshot = result.get("snapshot")
527
- if snapshot == (int(self.current_page), int(self.rows_per_page)):
528
- try:
529
- self._total_sessions = int(result.get("total_sessions") or 0)
530
- except Exception:
531
- self._total_sessions = 0
532
- try:
533
- self._stats = dict(result.get("stats") or {})
534
- except Exception:
535
- self._stats = {}
536
- try:
537
- self._sessions = list(result.get("sessions") or [])
538
- self._sessions_by_id = {s["id"]: s for s in self._sessions}
539
- except Exception:
540
- self._sessions = []
541
- self._sessions_by_id = {}
542
- self._update_display()
472
+ try:
473
+ self._total_sessions = int(result.get("total_sessions") or 0)
474
+ except Exception:
475
+ self._total_sessions = 0
476
+ try:
477
+ self._stats = dict(result.get("stats") or {})
478
+ except Exception:
479
+ self._stats = {}
480
+ try:
481
+ self._sessions = list(result.get("sessions") or [])
482
+ self._sessions_by_id = {s["id"]: s for s in self._sessions}
483
+ except Exception:
484
+ self._sessions = []
485
+ self._sessions_by_id = {}
486
+ self._update_display()
543
487
 
544
- if self._pending_refresh_snapshot is not None:
488
+ if self._pending_refresh:
545
489
  self.refresh_data()
546
490
 
547
491
  def _start_share_export(self) -> None:
@@ -772,8 +716,8 @@ class SessionsTable(Container):
772
716
  suffix = " (copied to clipboard)" if copied else ""
773
717
  self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
774
718
 
775
- def _collect_snapshot(self, page: int, rows_per_page: int) -> dict:
776
- """Collect sessions + stats for a single page (background thread)."""
719
+ def _collect_all_sessions(self) -> dict:
720
+ """Collect all sessions + stats (background thread)."""
777
721
  total_sessions: int = 0
778
722
  stats: dict = {}
779
723
  sessions: list[dict] = []
@@ -810,9 +754,7 @@ class SessionsTable(Container):
810
754
  "gemini": stats_row[3] if stats_row else 0,
811
755
  }
812
756
 
813
- # Get paginated sessions
814
- # Use cached total_turns instead of subquery for performance
815
- offset = (int(page) - 1) * int(rows_per_page)
757
+ # Get all sessions (up to MAX_SESSIONS)
816
758
  try:
817
759
  rows = conn.execute(
818
760
  """
@@ -827,9 +769,9 @@ class SessionsTable(Container):
827
769
  s.shared_by
828
770
  FROM sessions s
829
771
  ORDER BY s.last_activity_at DESC
830
- LIMIT ? OFFSET ?
772
+ LIMIT ?
831
773
  """,
832
- (int(rows_per_page), int(offset)),
774
+ (self.MAX_SESSIONS,),
833
775
  ).fetchall()
834
776
  has_new_columns = True
835
777
  except Exception:
@@ -845,9 +787,9 @@ class SessionsTable(Container):
845
787
  s.total_turns
846
788
  FROM sessions s
847
789
  ORDER BY s.last_activity_at DESC
848
- LIMIT ? OFFSET ?
790
+ LIMIT ?
849
791
  """,
850
- (int(rows_per_page), int(offset)),
792
+ (self.MAX_SESSIONS,),
851
793
  ).fetchall()
852
794
  has_new_columns = False
853
795
 
@@ -869,7 +811,6 @@ class SessionsTable(Container):
869
811
  "claude": "Claude",
870
812
  "codex": "Codex",
871
813
  "gemini": "Gemini",
872
- "antigravity": "Antigravity",
873
814
  }
874
815
  source = source_map.get(session_type, session_type)
875
816
  project = Path(workspace).name if workspace else "-"
@@ -904,7 +845,7 @@ class SessionsTable(Container):
904
845
 
905
846
  sessions.append(
906
847
  {
907
- "index": offset + i + 1,
848
+ "index": i + 1,
908
849
  "id": session_id,
909
850
  "short_id": self._shorten_session_id(session_id),
910
851
  "source": source,
@@ -917,13 +858,12 @@ class SessionsTable(Container):
917
858
  }
918
859
  )
919
860
  except Exception as e:
920
- logger.error(f"_collect_snapshot failed: {e}\n{traceback.format_exc()}")
861
+ logger.error(f"_collect_all_sessions failed: {e}\n{traceback.format_exc()}")
921
862
  total_sessions = 0
922
863
  stats = {}
923
864
  sessions = []
924
865
 
925
866
  return {
926
- "snapshot": (int(page), int(rows_per_page)),
927
867
  "total_sessions": total_sessions,
928
868
  "stats": stats,
929
869
  "sessions": sessions,
@@ -939,18 +879,23 @@ class SessionsTable(Container):
939
879
 
940
880
  # Update table
941
881
  table = self.query_one("#sessions-table", DataTable)
942
- selected_session_id: Optional[str] = None
943
- try:
944
- if table.row_count > 0:
945
- selected_session_id = str(
946
- table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
947
- )
948
- except Exception:
949
- selected_session_id = None
882
+
883
+ # Use saved cursor position if available, otherwise try to get current
884
+ restore_session_id = self._saved_cursor_session_id
885
+ if restore_session_id is None:
886
+ try:
887
+ if table.row_count > 0:
888
+ restore_session_id = str(
889
+ table.coordinate_to_cell_key(table.cursor_coordinate)[0].value
890
+ )
891
+ except Exception:
892
+ restore_session_id = None
893
+
950
894
  table.clear()
951
895
 
952
- # Always enable horizontal scrollbar
896
+ # Always enable scrollbars
953
897
  table.styles.overflow_x = "auto"
898
+ table.styles.overflow_y = "auto"
954
899
  table.show_horizontal_scrollbar = True
955
900
 
956
901
  for session in self._sessions:
@@ -979,10 +924,10 @@ class SessionsTable(Container):
979
924
  )
980
925
 
981
926
  if table.row_count > 0:
982
- if selected_session_id:
927
+ if restore_session_id:
983
928
  try:
984
929
  table.cursor_coordinate = (
985
- table.get_row_index(selected_session_id),
930
+ table.get_row_index(restore_session_id),
986
931
  0,
987
932
  )
988
933
  except Exception:
@@ -990,12 +935,8 @@ class SessionsTable(Container):
990
935
  else:
991
936
  table.cursor_coordinate = (0, 0)
992
937
 
993
- # Update pagination info
994
- total_pages = self._get_total_pages()
995
- pagination_widget = self.query_one("#pagination-info", Static)
996
- pagination_widget.update(
997
- f"[dim]Page {self.current_page}/{total_pages} ({self._total_sessions} total) │ (p) prev (n) next[/dim]"
998
- )
938
+ # Clear saved cursor position after restore
939
+ self._saved_cursor_session_id = None
999
940
 
1000
941
  def _shorten_session_id(self, session_id: str) -> str:
1001
942
  """Shorten a session ID for display."""
@@ -318,7 +318,6 @@ class WatcherPanel(Container, can_focus=True):
318
318
  ("Claude", "~/.claude/projects/", config.auto_detect_claude),
319
319
  ("Codex", "~/.codex/sessions/", config.auto_detect_codex),
320
320
  ("Gemini", "~/.gemini/tmp/", config.auto_detect_gemini),
321
- ("Antigravity", "~/.gemini/antigravity/brain/", config.auto_detect_antigravity),
322
321
  ]
323
322
  except Exception:
324
323
  monitored_paths = []
@@ -426,7 +425,6 @@ class WatcherPanel(Container, can_focus=True):
426
425
  "claude": "Claude",
427
426
  "codex": "Codex",
428
427
  "gemini": "Gemini",
429
- "antigravity": "Antigravity",
430
428
  }
431
429
  source = source_map.get(session_type, session_type)
432
430
  project = Path(workspace).name if workspace else "-"
realign/db/sqlite_db.py CHANGED
@@ -999,8 +999,7 @@ class SQLiteDatabase(DatabaseInterface):
999
999
  # - If the caller requested regeneration (`skip_dedup=True`), we must allow requeue.
1000
1000
  # - If the DB is missing the corresponding turn row (historical bug / manual DB edits),
1001
1001
  # requeue so the system can self-heal instead of getting stuck in a "done but missing" state.
1002
- # - For antigravity, turns may update-in-place, so allow requeue even if done.
1003
- requeue_done = bool(skip_dedup) or (session_type or "").lower() in ("antigravity",)
1002
+ requeue_done = bool(skip_dedup)
1004
1003
  try:
1005
1004
  conn = self._get_connection()
1006
1005
  row = conn.execute(
@@ -9,7 +9,7 @@ from ..db.sqlite_db import SQLiteDatabase
9
9
  from ..db.base import SessionRecord
10
10
  from ..db.locks import lease_lock, lock_key_for_event_summary, make_lock_owner
11
11
  from .debouncer import Debouncer
12
- from ..llm_client import call_llm, extract_json
12
+ from ..llm_client import extract_json, call_llm_cloud
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
@@ -201,41 +201,82 @@ def _generate_event_summary_llm(sessions: List[SessionRecord]) -> Tuple[str, str
201
201
  }
202
202
  )
203
203
 
204
- system_prompt = _get_event_summary_prompt()
205
-
206
- user_prompt = json.dumps(
207
- {
208
- "total_sessions": len(sessions),
209
- "sessions": sessions_data,
210
- },
211
- ensure_ascii=False,
212
- indent=2,
213
- )
214
-
204
+ # Try cloud provider first if user is logged in
215
205
  try:
216
- # Use unified LLM client
217
- _, response = call_llm(
218
- system_prompt=system_prompt,
219
- user_prompt=user_prompt,
220
- provider="auto", # Try Claude first, fallback to OpenAI
221
- max_tokens=500,
222
- purpose="event_summary",
223
- )
224
-
225
- if not response:
226
- logger.warning("LLM returned empty response, using fallback")
227
- return _fallback_event_summary(sessions)
228
-
229
- result = extract_json(response)
230
-
231
- title = result.get("event_title", "Untitled Event")[:100]
232
- description = result.get("event_description", "")
233
-
234
- return title, description
235
-
236
- except Exception as e:
237
- logger.warning(f"LLM event summary failed, using fallback: {e}")
238
- return _fallback_event_summary(sessions)
206
+ from ..auth import is_logged_in
207
+
208
+ if is_logged_in():
209
+ logger.debug("Attempting cloud LLM for event summary")
210
+ # Load user custom prompt if available
211
+ custom_prompt = None
212
+ user_prompt_path = Path.home() / ".aline" / "prompts" / "event_summary.md"
213
+ try:
214
+ if user_prompt_path.exists():
215
+ custom_prompt = user_prompt_path.read_text(encoding="utf-8").strip()
216
+ except Exception:
217
+ pass
218
+
219
+ _, result = call_llm_cloud(
220
+ task="event_summary",
221
+ payload={"sessions": sessions_data},
222
+ custom_prompt=custom_prompt,
223
+ silent=True,
224
+ )
225
+
226
+ if result:
227
+ title = result.get("event_title", "Untitled Event")[:100]
228
+ description = result.get("event_description", "")
229
+ logger.info(f"Cloud LLM event summary success: title={title[:50]}...")
230
+ return title, description
231
+ else:
232
+ # Cloud LLM failed, use fallback (local fallback disabled)
233
+ logger.warning("Cloud LLM event summary failed, using fallback")
234
+ return _fallback_event_summary(sessions)
235
+ except ImportError:
236
+ logger.debug("Auth module not available, skipping cloud LLM")
237
+
238
+ # User not logged in, use fallback (local fallback disabled)
239
+ logger.warning("Not logged in, cannot use cloud LLM for event summary")
240
+ return _fallback_event_summary(sessions)
241
+
242
+ # =========================================================================
243
+ # LOCAL LLM FALLBACK DISABLED - Code kept for reference
244
+ # =========================================================================
245
+ # system_prompt = _get_event_summary_prompt()
246
+ #
247
+ # user_prompt = json.dumps(
248
+ # {
249
+ # "total_sessions": len(sessions),
250
+ # "sessions": sessions_data,
251
+ # },
252
+ # ensure_ascii=False,
253
+ # indent=2,
254
+ # )
255
+ #
256
+ # try:
257
+ # # Use unified LLM client
258
+ # _, response = call_llm(
259
+ # system_prompt=system_prompt,
260
+ # user_prompt=user_prompt,
261
+ # provider="auto", # Try Claude first, fallback to OpenAI
262
+ # max_tokens=500,
263
+ # purpose="event_summary",
264
+ # )
265
+ #
266
+ # if not response:
267
+ # logger.warning("LLM returned empty response, using fallback")
268
+ # return _fallback_event_summary(sessions)
269
+ #
270
+ # result = extract_json(response)
271
+ #
272
+ # title = result.get("event_title", "Untitled Event")[:100]
273
+ # description = result.get("event_description", "")
274
+ #
275
+ # return title, description
276
+ #
277
+ # except Exception as e:
278
+ # logger.warning(f"LLM event summary failed, using fallback: {e}")
279
+ # return _fallback_event_summary(sessions)
239
280
 
240
281
 
241
282
  def _fallback_event_summary(sessions: List[SessionRecord]) -> Tuple[str, str]:
@@ -10,7 +10,7 @@ from typing import List, Tuple, Optional
10
10
  from ..db.sqlite_db import SQLiteDatabase
11
11
  from ..db.base import TurnRecord
12
12
  from ..db.locks import lease_lock, lock_key_for_session_summary, make_lock_owner
13
- from ..llm_client import call_llm, extract_json
13
+ from ..llm_client import extract_json, call_llm_cloud
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -267,41 +267,82 @@ def _generate_session_summary_llm(turns: List[TurnRecord]) -> Tuple[str, str]:
267
267
  }
268
268
  )
269
269
 
270
- system_prompt = _get_session_summary_prompt()
271
-
272
- user_prompt = json.dumps(
273
- {
274
- "total_turns": len(turns),
275
- "turns": turns_data,
276
- },
277
- ensure_ascii=False,
278
- indent=2,
279
- )
280
-
270
+ # Try cloud provider first if user is logged in
281
271
  try:
282
- # Use unified LLM client
283
- _, response = call_llm(
284
- system_prompt=system_prompt,
285
- user_prompt=user_prompt,
286
- provider="auto", # Try Claude first, fallback to OpenAI
287
- max_tokens=500,
288
- purpose="session_summary",
289
- )
290
-
291
- if not response:
292
- logger.warning("LLM returned empty response, using fallback")
293
- return _fallback_summary(turns)
294
-
295
- result = extract_json(response)
272
+ from ..auth import is_logged_in
296
273
 
297
- title = result.get("session_title", "Untitled Session")[:80]
298
- summary = result.get("session_summary", "")
274
+ if is_logged_in():
275
+ logger.debug("Attempting cloud LLM for session summary")
276
+ # Load user custom prompt if available
277
+ custom_prompt = None
278
+ user_prompt_path = Path.home() / ".aline" / "prompts" / "session_summary.md"
279
+ try:
280
+ if user_prompt_path.exists():
281
+ custom_prompt = user_prompt_path.read_text(encoding="utf-8").strip()
282
+ except Exception:
283
+ pass
299
284
 
300
- return title, summary
285
+ _, result = call_llm_cloud(
286
+ task="session_summary",
287
+ payload={"turns": turns_data},
288
+ custom_prompt=custom_prompt,
289
+ silent=True,
290
+ )
301
291
 
302
- except Exception as e:
303
- logger.warning(f"LLM session summary failed, using fallback: {e}")
304
- return _fallback_summary(turns)
292
+ if result:
293
+ title = result.get("session_title", "Untitled Session")[:80]
294
+ summary = result.get("session_summary", "")
295
+ logger.info(f"Cloud LLM session summary success: title={title[:50]}...")
296
+ return title, summary
297
+ else:
298
+ # Cloud LLM failed, use fallback (local fallback disabled)
299
+ logger.warning("Cloud LLM session summary failed, using fallback")
300
+ return _fallback_summary(turns)
301
+ except ImportError:
302
+ logger.debug("Auth module not available, skipping cloud LLM")
303
+
304
+ # User not logged in, use fallback (local fallback disabled)
305
+ logger.warning("Not logged in, cannot use cloud LLM for session summary")
306
+ return _fallback_summary(turns)
307
+
308
+ # =========================================================================
309
+ # LOCAL LLM FALLBACK DISABLED - Code kept for reference
310
+ # =========================================================================
311
+ # system_prompt = _get_session_summary_prompt()
312
+ #
313
+ # user_prompt = json.dumps(
314
+ # {
315
+ # "total_turns": len(turns),
316
+ # "turns": turns_data,
317
+ # },
318
+ # ensure_ascii=False,
319
+ # indent=2,
320
+ # )
321
+ #
322
+ # try:
323
+ # # Use unified LLM client
324
+ # _, response = call_llm(
325
+ # system_prompt=system_prompt,
326
+ # user_prompt=user_prompt,
327
+ # provider="auto", # Try Claude first, fallback to OpenAI
328
+ # max_tokens=500,
329
+ # purpose="session_summary",
330
+ # )
331
+ #
332
+ # if not response:
333
+ # logger.warning("LLM returned empty response, using fallback")
334
+ # return _fallback_summary(turns)
335
+ #
336
+ # result = extract_json(response)
337
+ #
338
+ # title = result.get("session_title", "Untitled Session")[:80]
339
+ # summary = result.get("session_summary", "")
340
+ #
341
+ # return title, summary
342
+ #
343
+ # except Exception as e:
344
+ # logger.warning(f"LLM session summary failed, using fallback: {e}")
345
+ # return _fallback_summary(turns)
305
346
 
306
347
 
307
348
  def _fallback_summary(turns: List[TurnRecord]) -> Tuple[str, str]: