chat-console 0.2.9__py3-none-any.whl → 0.2.98__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.
app/ui/chat_interface.py CHANGED
@@ -8,9 +8,9 @@ import logging
8
8
  from textual.app import ComposeResult
9
9
  from textual.containers import Container, ScrollableContainer, Vertical
10
10
  from textual.reactive import reactive
11
- from textual.widgets import Button, Input, Label, Static
11
+ from textual.widgets import Button, Input, Label, Static # Removed RichLog from here
12
12
  from textual.widget import Widget
13
- from textual.widgets import RichLog
13
+ # Removed RichLog import as MessageDisplay will inherit from Static
14
14
  from textual.message import Message
15
15
  from textual.binding import Binding
16
16
 
@@ -53,16 +53,22 @@ class SendButton(Button):
53
53
  self.styles.text_opacity = 100
54
54
  self.styles.text_style = "bold"
55
55
 
56
- class MessageDisplay(RichLog):
57
- """Widget to display a single message"""
56
+ class MessageDisplay(Static): # Inherit from Static instead of RichLog
57
+ """Widget to display a single message using Static"""
58
58
 
59
59
  DEFAULT_CSS = """
60
60
  MessageDisplay {
61
61
  width: 100%;
62
- height: auto;
62
+ height: auto; /* Let height adjust automatically */
63
+ min-height: 1; /* Ensure minimum height */
64
+ min-width: 60; /* Set a reasonable minimum width to avoid constant width adjustment */
63
65
  margin: 1 0;
64
- overflow: auto;
65
66
  padding: 1;
67
+ text-wrap: wrap; /* Explicitly enable text wrapping via CSS */
68
+ content-align: left top; /* Anchor content to top-left */
69
+ overflow-y: visible; /* Allow content to expand */
70
+ box-sizing: border-box; /* Include padding in size calculations */
71
+ transitions: none; /* Disable any transitions that might cause animation */
66
72
  }
67
73
 
68
74
  MessageDisplay.user-message {
@@ -90,14 +96,16 @@ class MessageDisplay(RichLog):
90
96
  highlight_code: bool = True,
91
97
  name: Optional[str] = None
92
98
  ):
99
+ # Initialize Static with empty content and necessary parameters
100
+ # Static supports markup but handles wrap differently via styles
93
101
  super().__init__(
94
- highlight=True,
102
+ "", # Initialize with empty content initially
95
103
  markup=True,
96
- wrap=True,
97
104
  name=name
98
105
  )
106
+ # Enable text wrapping via CSS (already set in DEFAULT_CSS)
99
107
  self.message = message
100
- self.highlight_code = highlight_code
108
+ self.highlight_code = highlight_code # Keep this for potential future use or logic
101
109
 
102
110
  def on_mount(self) -> None:
103
111
  """Handle mount event"""
@@ -109,18 +117,23 @@ class MessageDisplay(RichLog):
109
117
  elif self.message.role == "system":
110
118
  self.add_class("system-message")
111
119
 
112
- # Initial content
113
- self.write(self._format_content(self.message.content))
120
+ # Initial content using Static's update method
121
+ self.update(self._format_content(self.message.content))
114
122
 
115
123
  async def update_content(self, content: str) -> None:
116
- """Update the message content"""
124
+ """Update the message content using Static.update()"""
125
+ # Update the stored message object content first
117
126
  self.message.content = content
118
- self.clear()
119
- self.write(self._format_content(content))
120
- # Force a refresh after writing
121
- self.refresh(layout=True)
122
- # Wait a moment for the layout to update
123
- await asyncio.sleep(0.05)
127
+
128
+ # Format with fixed-width placeholder to minimize layout shifts
129
+ # This avoids text reflowing as new tokens arrive
130
+ formatted_content = self._format_content(content)
131
+
132
+ # Update Static widget with minimal refresh
133
+ self.update(formatted_content)
134
+
135
+ # Important: Don't call refresh() here - let the parent handle timing
136
+ # This prevents constant layout recalculation on each token
124
137
 
125
138
  def _format_content(self, content: str) -> str:
126
139
  """Format message content with timestamp"""
@@ -164,6 +177,9 @@ class ChatInterface(Container):
164
177
  border-bottom: solid $primary-darken-2;
165
178
  overflow: auto;
166
179
  padding: 0 1;
180
+ content-align: left top; /* Keep content anchored at top */
181
+ box-sizing: border-box;
182
+ scrollbar-size: 1 1; /* Smaller scrollbars for more stability */
167
183
  }
168
184
 
169
185
  #input-area {
@@ -243,7 +259,7 @@ class ChatInterface(Container):
243
259
  yield MessageDisplay(message, highlight_code=CONFIG["highlight_code"])
244
260
  with Container(id="input-area"):
245
261
  yield Container(
246
- Label("▪▪▪ Generating response...", id="loading-text", markup=True),
262
+ Label("▪▪▪ Generating response...", id="loading-text", markup=False),
247
263
  id="loading-indicator"
248
264
  )
249
265
  with Container(id="controls"):
app/ui/model_browser.py CHANGED
@@ -684,23 +684,124 @@ class ModelBrowser(Container):
684
684
 
685
685
  async def _run_selected_model(self) -> None:
686
686
  """Set the selected model as the active model in the main app"""
687
- # Get selected model based on current tab
688
- model_id = self._get_selected_model_id()
687
+ # Import debug_log function from main
688
+ from app.main import debug_log
689
+
690
+ debug_log(f"Entering _run_selected_model for tab {self.current_tab}")
689
691
 
692
+ # Try several ways to get a valid model ID
693
+ model_id = ""
694
+
695
+ # First try the selected_model_id property
696
+ if self.selected_model_id and self.selected_model_id.strip():
697
+ debug_log(f"Using model_id from selected_model_id: {self.selected_model_id}")
698
+ logger.info(f"Using model_id from selected_model_id: {self.selected_model_id}")
699
+ model_id = self.selected_model_id
700
+
701
+ # If that didn't work, try getting it through the getter method
690
702
  if not model_id:
691
- self.notify("No model selected", severity="warning")
692
- return
703
+ debug_log("Trying to get model_id from _get_selected_model_id method")
704
+ logger.info("Trying to get model_id from _get_selected_model_id method")
705
+ model_id = self._get_selected_model_id()
706
+ debug_log(f"_get_selected_model_id returned: '{model_id}'")
707
+
708
+ # As a last resort, if we're in local tab, try to just get the first model
709
+ if not model_id and self.current_tab == "local":
710
+ debug_log("Trying fallback to first local model")
693
711
 
712
+ # Extra defensive check for local_models
713
+ if not self.local_models:
714
+ debug_log("local_models list is empty or None")
715
+ else:
716
+ debug_log(f"local_models has {len(self.local_models)} items")
717
+ try:
718
+ # Check if the list is valid and not empty
719
+ if len(self.local_models) > 0:
720
+ first_model = self.local_models[0]
721
+ debug_log(f"First local model: {repr(first_model)}")
722
+
723
+ # Very defensive checks
724
+ if first_model is None:
725
+ debug_log("First model is None")
726
+ elif not isinstance(first_model, dict):
727
+ debug_log(f"First model is not a dict: {type(first_model)}")
728
+ elif "id" not in first_model:
729
+ debug_log(f"First model has no 'id' key: {first_model}")
730
+ else:
731
+ model_id = first_model.get("id", "")
732
+ debug_log(f"Falling back to first model in list: {model_id}")
733
+ logger.info(f"Falling back to first model in list: {model_id}")
734
+ else:
735
+ debug_log("local_models list is empty")
736
+ except Exception as e:
737
+ debug_log(f"Error getting first local model: {str(e)}")
738
+ logger.error(f"Error getting first local model: {str(e)}")
739
+
740
+ # Or if we're in available tab, try to get name of first available model
741
+ if not model_id and self.current_tab == "available":
742
+ debug_log("Trying fallback to first available model")
743
+
744
+ # Extra defensive check for available_models
745
+ if not self.available_models:
746
+ debug_log("available_models list is empty or None")
747
+ else:
748
+ debug_log(f"available_models has {len(self.available_models)} items")
749
+ try:
750
+ # Check if the list is valid and not empty
751
+ if len(self.available_models) > 0:
752
+ first_model = self.available_models[0]
753
+ debug_log(f"First available model: {repr(first_model)}")
754
+
755
+ # Very defensive checks
756
+ if first_model is None:
757
+ debug_log("First available model is None")
758
+ elif not isinstance(first_model, dict):
759
+ debug_log(f"First available model is not a dict: {type(first_model)}")
760
+ elif "name" not in first_model:
761
+ debug_log(f"First available model has no 'name' key: {first_model}")
762
+ else:
763
+ model_id = first_model.get("name", "")
764
+ debug_log(f"Falling back to first available model: {model_id}")
765
+ logger.info(f"Falling back to first available model: {model_id}")
766
+ else:
767
+ debug_log("available_models list is empty")
768
+ except Exception as e:
769
+ debug_log(f"Error getting first available model: {str(e)}")
770
+ logger.error(f"Error getting first available model: {str(e)}")
771
+
772
+ # Last resort - hardcoded fallback to a common model
773
+ if not model_id:
774
+ debug_log("All attempts to get model_id failed, checking if we should use default")
775
+ # Only use default if we're in the available models tab, otherwise notify user
776
+ if self.current_tab == "available":
777
+ debug_log("Using 'llama3' as last-resort default")
778
+ model_id = "llama3"
779
+ self.notify("No model selected, using llama3 as default", severity="warning")
780
+ else:
781
+ debug_log("No model_id found and no default for local tab")
782
+ self.notify("No model selected", severity="warning")
783
+ return
784
+
785
+ debug_log(f"Final model_id: {model_id}")
786
+ logger.info(f"Setting model to: {model_id}")
787
+
694
788
  try:
695
789
  # Set the model in the app
696
790
  if hasattr(self.app, "selected_model"):
791
+ debug_log("Setting model in the app")
697
792
  self.app.selected_model = model_id
793
+ debug_log("Updating app info")
698
794
  self.app.update_app_info() # Update app info to show new model
795
+ debug_log("Model set successfully")
699
796
  self.notify(f"Model set to: {model_id}", severity="success")
797
+ debug_log("Closing model browser screen")
700
798
  self.app.pop_screen() # Close the model browser screen
701
799
  else:
800
+ debug_log("app does not have selected_model attribute")
702
801
  self.notify("Cannot set model: app interface not available", severity="error")
703
802
  except Exception as e:
803
+ debug_log(f"Error setting model: {str(e)}")
804
+ logger.error(f"Error setting model: {str(e)}")
704
805
  self.notify(f"Error setting model: {str(e)}", severity="error")
705
806
 
706
807
  async def _pull_selected_model(self) -> None:
@@ -1063,72 +1164,331 @@ class ModelBrowser(Container):
1063
1164
 
1064
1165
  def _get_selected_model_id(self) -> str:
1065
1166
  """Get the ID of the currently selected model"""
1167
+ # Import debug_log function from main
1168
+ from app.main import debug_log
1169
+
1170
+ debug_log(f"Entering _get_selected_model_id for tab {self.current_tab}")
1171
+
1066
1172
  try:
1067
1173
  if self.current_tab == "local":
1174
+ debug_log("Processing local models tab")
1068
1175
  table = self.query_one("#local-models-table", DataTable)
1069
- if table.cursor_row is not None:
1070
- row = table.get_row_at(table.cursor_row)
1071
- # Get model ID from local models list
1176
+
1177
+ # Log table state
1178
+ debug_log(f"Table cursor_row: {table.cursor_row}, row_count: {table.row_count}")
1179
+
1180
+ if table.cursor_row is not None and table.row_count > 0:
1181
+ # Safety checks on cursor row
1182
+ if table.cursor_row < 0 or table.cursor_row >= table.row_count:
1183
+ debug_log(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
1184
+ logger.error(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
1185
+ return ""
1186
+
1072
1187
  try:
1073
- if row and len(row) > 0:
1074
- row_name = str(row[0]) if row[0] is not None else ""
1075
- for model in self.local_models:
1076
- if model["name"] == row_name:
1077
- return model["id"]
1078
- except (IndexError, TypeError) as e:
1079
- logger.error(f"Error processing row data: {str(e)}")
1188
+ debug_log(f"Getting row at cursor position {table.cursor_row}")
1189
+ row = table.get_row_at(table.cursor_row)
1190
+ debug_log(f"Row data: {repr(row)}")
1191
+
1192
+ # Use a more permissive approach that works with any indexable type
1193
+ if row and hasattr(row, '__getitem__') and len(row) > 0:
1194
+ try:
1195
+ row_value = row[0]
1196
+ row_name = str(row_value) if row_value is not None else ""
1197
+ debug_log(f"Row name extracted: '{row_name}'")
1198
+
1199
+ # Check if local_models is valid
1200
+ if not self.local_models:
1201
+ debug_log("local_models list is empty")
1202
+ return ""
1203
+
1204
+ debug_log(f"Searching through {len(self.local_models)} local models")
1205
+ for i, model in enumerate(self.local_models):
1206
+ try:
1207
+ # Defensively check if model is a dict and has required keys
1208
+ if not isinstance(model, dict):
1209
+ debug_log(f"Model at index {i} is not a dict: {repr(model)}")
1210
+ continue
1211
+
1212
+ model_name = model.get("name")
1213
+ model_id = model.get("id")
1214
+
1215
+ if model_name is None:
1216
+ debug_log(f"Model at index {i} has no name: {repr(model)}")
1217
+ continue
1218
+
1219
+ debug_log(f"Comparing '{model_name}' with '{row_name}'")
1220
+ if model_name == row_name:
1221
+ debug_log(f"Found matching model: {model_id}")
1222
+ return model_id
1223
+ except Exception as model_err:
1224
+ debug_log(f"Error processing model at index {i}: {str(model_err)}")
1225
+ logger.error(f"Error processing model: {model_err}")
1226
+ continue
1227
+
1228
+ # If we got here, no match was found
1229
+ debug_log(f"No matching model found for '{row_name}'")
1230
+ except Exception as extract_err:
1231
+ debug_log(f"Error extracting row name: {str(extract_err)}")
1232
+ else:
1233
+ debug_log(f"Invalid row data: Row doesn't support indexing, is empty, or is None: {repr(row)}")
1234
+ return ""
1235
+
1236
+ except (IndexError, TypeError, AttributeError) as e:
1237
+ debug_log(f"Error processing row data in _get_selected_model_id: {str(e)}")
1238
+ logger.error(f"Error processing row data in _get_selected_model_id: {str(e)}")
1080
1239
  else:
1240
+ debug_log("Processing available models tab")
1081
1241
  table = self.query_one("#available-models-table", DataTable)
1082
- if table.cursor_row is not None:
1242
+
1243
+ # Log table state
1244
+ debug_log(f"Available table cursor_row: {table.cursor_row}, row_count: {table.row_count}")
1245
+
1246
+ if table.cursor_row is not None and table.row_count > 0:
1247
+ # Safety checks on cursor row
1248
+ if table.cursor_row < 0 or table.cursor_row >= table.row_count:
1249
+ debug_log(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
1250
+ logger.error(f"Invalid cursor row {table.cursor_row} for table with {table.row_count} rows")
1251
+ # Try to select first row instead of returning empty
1252
+ table.cursor_row = 0
1253
+ debug_log("Reset cursor_row to 0")
1254
+
1083
1255
  try:
1256
+ debug_log(f"Getting row at cursor position {table.cursor_row}")
1084
1257
  row = table.get_row_at(table.cursor_row)
1085
- # Return the model name as ID
1086
- if row and len(row) > 0:
1087
- return str(row[0]) if row[0] is not None else ""
1088
- except Exception as e:
1258
+ debug_log(f"Row data: {repr(row)}")
1259
+
1260
+ # Use a more permissive approach that works with any indexable type
1261
+ if row and hasattr(row, '__getitem__') and len(row) > 0:
1262
+ try:
1263
+ row_value = row[0]
1264
+ model_name = str(row_value) if row_value is not None else ""
1265
+ debug_log(f"Available model selected: '{model_name}'")
1266
+ logger.info(f"Selected available model: {model_name}")
1267
+ return model_name
1268
+ except Exception as extract_err:
1269
+ debug_log(f"Error extracting row name: {str(extract_err)}")
1270
+ return ""
1271
+ else:
1272
+ debug_log(f"Invalid row data: Row doesn't support indexing, is empty, or is None: {repr(row)}")
1273
+ return ""
1274
+
1275
+ except (IndexError, TypeError, AttributeError) as e:
1276
+ debug_log(f"Error getting row at cursor: {str(e)}")
1089
1277
  logger.error(f"Error getting row at cursor: {str(e)}")
1090
1278
 
1091
1279
  # If we couldn't get a valid row, check if there are any rows and select the first one
1092
- if table.row_count > 0:
1280
+ debug_log("Trying fallback to first row")
1281
+ if hasattr(table, 'row_count') and table.row_count > 0:
1093
1282
  try:
1094
1283
  # Select the first row and get its ID
1284
+ debug_log(f"Setting cursor_row to 0 and getting first row")
1095
1285
  table.cursor_row = 0
1096
1286
  row = table.get_row_at(0)
1097
- if row and len(row) > 0:
1098
- return str(row[0]) if row[0] is not None else ""
1099
- except Exception as e:
1287
+ debug_log(f"First row data: {repr(row)}")
1288
+
1289
+ # Use a more permissive approach that works with any indexable type
1290
+ if row and hasattr(row, '__getitem__') and len(row) > 0:
1291
+ try:
1292
+ row_value = row[0]
1293
+ model_name = str(row_value) if row_value is not None else ""
1294
+ debug_log(f"First available model selected: '{model_name}'")
1295
+ logger.info(f"Selected first available model: {model_name}")
1296
+ return model_name
1297
+ except Exception as extract_err:
1298
+ debug_log(f"Error extracting first row name: {str(extract_err)}")
1299
+ return ""
1300
+ else:
1301
+ debug_log(f"Invalid first row data: Row doesn't support indexing, is empty, or is None: {repr(row)}")
1302
+ return ""
1303
+
1304
+ except (IndexError, TypeError, AttributeError) as e:
1305
+ debug_log(f"Error selecting first row: {str(e)}")
1100
1306
  logger.error(f"Error selecting first row: {str(e)}")
1307
+ else:
1308
+ debug_log("Table has no rows or row_count attribute missing")
1101
1309
  except Exception as e:
1310
+ debug_log(f"Unexpected error in _get_selected_model_id: {str(e)}")
1102
1311
  logger.error(f"Error in _get_selected_model_id: {str(e)}")
1103
1312
 
1313
+ debug_log("No model ID found, returning empty string")
1104
1314
  return ""
1105
1315
 
1106
1316
  def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
1107
1317
  """Handle row selection in data tables"""
1318
+ # Import debug_log function from main
1319
+ from app.main import debug_log
1320
+
1321
+ debug_log(f"Entering on_data_table_row_selected")
1322
+
1108
1323
  # Set selected model ID based on the selected row
1109
- if event.data_table.id == "local-models-table":
1110
- row = event.data_table.get_row_at(event.cursor_row)
1111
- # Find the model ID from the display name
1112
- try:
1113
- if row and len(row) > 0:
1114
- row_name = str(row[0]) if row[0] is not None else ""
1115
- for model in self.local_models:
1116
- if model["name"] == row_name:
1117
- self.selected_model_id = model["id"]
1118
- break
1119
- except (IndexError, TypeError) as e:
1120
- logger.error(f"Error processing row data: {str(e)}")
1121
- elif event.data_table.id == "available-models-table":
1122
- row = event.data_table.get_row_at(event.cursor_row)
1123
- # Model name is used as ID
1124
- try:
1125
- if row and len(row) > 0:
1126
- self.selected_model_id = str(row[0]) if row[0] is not None else ""
1127
- else:
1324
+ try:
1325
+ # Check if event has valid structure
1326
+ if not hasattr(event, 'data_table') or not event.data_table:
1327
+ debug_log("Event has no data_table attribute or it is None")
1328
+ logger.error("Invalid event in on_data_table_row_selected")
1329
+ return
1330
+
1331
+ if not hasattr(event, 'cursor_row'):
1332
+ debug_log("Event has no cursor_row attribute")
1333
+ logger.error("Event has no cursor_row attribute")
1334
+ return
1335
+
1336
+ debug_log(f"Event cursor_row: {event.cursor_row}")
1337
+
1338
+ if not hasattr(event.data_table, 'id') or not event.data_table.id:
1339
+ debug_log("Data table has no ID attribute or it is empty")
1340
+ logger.error("Data table has no ID in on_data_table_row_selected")
1341
+ return
1342
+
1343
+ debug_log(f"Data table ID: {event.data_table.id}")
1344
+
1345
+ if event.data_table.id == "local-models-table":
1346
+ debug_log("Processing local models table")
1347
+ try:
1348
+ # Carefully get the row data
1349
+ if event.cursor_row is None or event.cursor_row < 0:
1350
+ debug_log(f"Invalid cursor row: {event.cursor_row}")
1351
+ logger.error(f"Invalid cursor row: {event.cursor_row}")
1352
+ return
1353
+
1354
+ # Check row count to avoid index errors
1355
+ if not hasattr(event.data_table, 'row_count'):
1356
+ debug_log("Data table has no row_count attribute")
1357
+ return
1358
+
1359
+ debug_log(f"Table row_count: {event.data_table.row_count}")
1360
+
1361
+ if event.data_table.row_count <= event.cursor_row:
1362
+ debug_log(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
1363
+ logger.error(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
1364
+ return
1365
+
1366
+ debug_log(f"Getting row at cursor position {event.cursor_row}")
1367
+ row = event.data_table.get_row_at(event.cursor_row)
1368
+ debug_log(f"Row data: {repr(row)}")
1369
+
1370
+ # Find the model ID from the display name with more permissive checks
1371
+ try:
1372
+ # Check if row is valid and can be indexed
1373
+ if row is None:
1374
+ debug_log("Row is None")
1375
+ return
1376
+
1377
+ if not hasattr(row, '__getitem__') or not hasattr(row, '__len__'):
1378
+ debug_log(f"Row doesn't support indexing or length: {type(row)}")
1379
+ return
1380
+
1381
+ if len(row) <= 0:
1382
+ debug_log("Row has no items")
1383
+ return
1384
+
1385
+ # Extract the first item (model name)
1386
+ try:
1387
+ row_value = row[0]
1388
+ row_name = str(row_value) if row_value is not None else ""
1389
+ debug_log(f"DataTable row selected with name: '{row_name}'")
1390
+ logger.info(f"DataTable row selected with name: {row_name}")
1391
+ except Exception as idx_err:
1392
+ debug_log(f"Error accessing row[0]: {str(idx_err)}")
1393
+ return
1394
+
1395
+ # Check if local_models is a valid list
1396
+ if not self.local_models:
1397
+ debug_log("local_models list is empty or None")
1398
+ return
1399
+
1400
+ debug_log(f"Searching through {len(self.local_models)} local models")
1401
+ for i, model in enumerate(self.local_models):
1402
+ try:
1403
+ # Use .get() for safer dictionary access
1404
+ model_name = model.get("name", None)
1405
+ if model_name is None:
1406
+ debug_log(f"Model at index {i} has no name")
1407
+ continue
1408
+
1409
+ debug_log(f"Comparing '{model_name}' with '{row_name}'")
1410
+ if model_name == row_name:
1411
+ model_id = model.get("id", "")
1412
+ debug_log(f"Found match. Setting selected_model_id to: '{model_id}'")
1413
+ logger.info(f"Setting selected_model_id to: {model_id}")
1414
+ self.selected_model_id = model_id
1415
+ break
1416
+ except Exception as model_err:
1417
+ debug_log(f"Error processing model at index {i}: {str(model_err)}")
1418
+ logger.error(f"Error matching local model: {str(model_err)}")
1419
+ continue
1420
+ except (IndexError, TypeError) as idx_err:
1421
+ debug_log(f"Error accessing row data: {str(idx_err)}")
1422
+ logger.error(f"Error accessing row data: {str(idx_err)}")
1423
+ except (IndexError, TypeError, AttributeError) as e:
1424
+ debug_log(f"Error processing local table row data: {str(e)}")
1425
+ logger.error(f"Error processing local table row data: {str(e)}")
1426
+ elif event.data_table.id == "available-models-table":
1427
+ debug_log("Processing available models table")
1428
+ try:
1429
+ # Similar safety checks for available models
1430
+ if event.cursor_row is None or event.cursor_row < 0:
1431
+ debug_log(f"Invalid cursor row: {event.cursor_row}")
1432
+ logger.error(f"Invalid cursor row: {event.cursor_row}")
1433
+ return
1434
+
1435
+ # Check row count to avoid index errors
1436
+ if not hasattr(event.data_table, 'row_count'):
1437
+ debug_log("Data table has no row_count attribute")
1438
+ return
1439
+
1440
+ debug_log(f"Available table row_count: {event.data_table.row_count}")
1441
+
1442
+ if event.data_table.row_count <= event.cursor_row:
1443
+ debug_log(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
1444
+ logger.error(f"Cursor row {event.cursor_row} exceeds row count {event.data_table.row_count}")
1445
+ return
1446
+
1447
+ debug_log(f"Getting row at cursor position {event.cursor_row}")
1448
+ row = event.data_table.get_row_at(event.cursor_row)
1449
+ debug_log(f"Row data: {repr(row)}")
1450
+
1451
+ # Model name is used as ID, with more permissive checks
1452
+ try:
1453
+ # Check if row is valid and can be indexed
1454
+ if row is None:
1455
+ debug_log("Row is None")
1456
+ return
1457
+
1458
+ if not hasattr(row, '__getitem__') or not hasattr(row, '__len__'):
1459
+ debug_log(f"Row doesn't support indexing or length: {type(row)}")
1460
+ return
1461
+
1462
+ if len(row) <= 0:
1463
+ debug_log("Row has no items")
1464
+ return
1465
+
1466
+ # Extract the first item (model name)
1467
+ try:
1468
+ row_value = row[0]
1469
+ model_name = str(row_value) if row_value is not None else ""
1470
+ debug_log(f"Available model selected: '{model_name}'")
1471
+ logger.info(f"Selected available model: {model_name}")
1472
+ self.selected_model_id = model_name
1473
+ except Exception as access_err:
1474
+ debug_log(f"Error accessing row[0]: {str(access_err)}")
1475
+ logger.error(f"Error accessing row[0]: {str(access_err)}")
1476
+ self.selected_model_id = ""
1477
+ except (IndexError, TypeError) as idx_err:
1478
+ debug_log(f"Error accessing available table row data: {str(idx_err)}")
1479
+ logger.error(f"Error accessing available table row data: {str(idx_err)}")
1480
+ self.selected_model_id = ""
1481
+ except (IndexError, TypeError, AttributeError) as e:
1482
+ debug_log(f"Error getting model ID from available table row: {str(e)}")
1483
+ logger.error(f"Error getting model ID from available table row: {str(e)}")
1128
1484
  self.selected_model_id = ""
1129
- except (IndexError, TypeError) as e:
1130
- logger.error(f"Error getting model ID from row: {str(e)}")
1131
- self.selected_model_id = ""
1485
+ else:
1486
+ debug_log(f"Unknown table ID: {event.data_table.id}")
1487
+ except Exception as e:
1488
+ # Catch-all for any other errors
1489
+ debug_log(f"Unexpected error in on_data_table_row_selected: {str(e)}")
1490
+ logger.error(f"Unexpected error in on_data_table_row_selected: {str(e)}")
1491
+ self.selected_model_id = ""
1132
1492
 
1133
1493
  def on_input_submitted(self, event: Input.Submitted) -> None:
1134
1494
  """Handle input submission (Enter key in search input)"""