chat-console 0.1.96.dev1__tar.gz → 0.2.0__tar.gz

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 (28) hide show
  1. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/PKG-INFO +1 -1
  2. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/main.py +222 -101
  3. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/ui/chat_interface.py +16 -13
  4. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/utils.py +25 -5
  5. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/chat_console.egg-info/PKG-INFO +1 -1
  6. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/setup.py +1 -1
  7. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/LICENSE +0 -0
  8. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/README.md +0 -0
  9. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/__init__.py +0 -0
  10. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/api/__init__.py +0 -0
  11. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/api/anthropic.py +0 -0
  12. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/api/base.py +0 -0
  13. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/api/ollama.py +0 -0
  14. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/api/openai.py +0 -0
  15. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/config.py +0 -0
  16. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/database.py +0 -0
  17. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/models.py +0 -0
  18. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/ui/__init__.py +0 -0
  19. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/ui/chat_list.py +0 -0
  20. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/ui/model_selector.py +0 -0
  21. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/ui/search.py +0 -0
  22. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/app/ui/styles.py +0 -0
  23. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/chat_console.egg-info/SOURCES.txt +0 -0
  24. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/chat_console.egg-info/dependency_links.txt +0 -0
  25. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/chat_console.egg-info/entry_points.txt +0 -0
  26. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/chat_console.egg-info/requires.txt +0 -0
  27. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/chat_console.egg-info/top_level.txt +0 -0
  28. {chat_console-0.1.96.dev1 → chat_console-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chat-console
3
- Version: 0.1.96.dev1
3
+ Version: 0.2.0
4
4
  Summary: A command-line interface for chatting with LLMs, storing chats and (future) rag interactions
5
5
  Home-page: https://github.com/wazacraftrfid/chat-console
6
6
  Author: Johnathan Greenaway
@@ -19,21 +19,24 @@ from openai import OpenAI
19
19
  from app.models import Message, Conversation
20
20
  from app.database import ChatDatabase
21
21
  from app.config import CONFIG, OPENAI_API_KEY, ANTHROPIC_API_KEY, OLLAMA_BASE_URL
22
- from app.ui.chat_interface import MessageDisplay
22
+ # Import InputWithFocus as well
23
+ from app.ui.chat_interface import MessageDisplay, InputWithFocus
23
24
  from app.ui.model_selector import ModelSelector, StyleSelector
24
25
  from app.ui.chat_list import ChatList
25
26
  from app.api.base import BaseModelClient
26
27
  from app.utils import generate_streaming_response, save_settings_to_config, generate_conversation_title # Import title function
28
+ # Import version here to avoid potential circular import issues at top level
29
+ from app import __version__
27
30
 
28
31
  # --- Remove SettingsScreen class entirely ---
29
32
 
30
33
  class HistoryScreen(Screen):
31
34
  """Screen for viewing chat history."""
32
-
35
+
33
36
  BINDINGS = [
34
37
  Binding("escape", "pop_screen", "Close"),
35
38
  ]
36
-
39
+
37
40
  CSS = """
38
41
  #history-container {
39
42
  width: 80; # Keep HistoryScreen CSS
@@ -42,29 +45,29 @@ class HistoryScreen(Screen):
42
45
  border: round $primary;
43
46
  padding: 1; # Keep HistoryScreen CSS
44
47
  }
45
-
48
+
46
49
  #title { # Keep HistoryScreen CSS
47
50
  width: 100%; # Keep HistoryScreen CSS
48
51
  content-align: center middle;
49
52
  text-align: center;
50
53
  padding-bottom: 1;
51
54
  }
52
-
55
+
53
56
  ListView { # Keep HistoryScreen CSS
54
57
  width: 100%; # Keep HistoryScreen CSS
55
58
  height: 1fr;
56
59
  border: solid $primary;
57
60
  }
58
-
61
+
59
62
  ListItem { # Keep HistoryScreen CSS
60
63
  padding: 1; # Keep HistoryScreen CSS
61
64
  border-bottom: solid $primary-darken-2;
62
65
  }
63
-
66
+
64
67
  ListItem:hover { # Keep HistoryScreen CSS
65
68
  background: $primary-darken-1; # Keep HistoryScreen CSS
66
69
  }
67
-
70
+
68
71
  #button-row { # Keep HistoryScreen CSS
69
72
  width: 100%; # Keep HistoryScreen CSS
70
73
  height: 3;
@@ -113,11 +116,16 @@ class HistoryScreen(Screen):
113
116
 
114
117
  class SimpleChatApp(App): # Keep SimpleChatApp class definition
115
118
  """Simplified Chat CLI application.""" # Keep SimpleChatApp docstring
116
-
117
- TITLE = "Chat CLI" # Keep SimpleChatApp TITLE
119
+
120
+ TITLE = "Chat Console"
118
121
  SUB_TITLE = "AI Chat Interface" # Keep SimpleChatApp SUB_TITLE
119
122
  DARK = True # Keep SimpleChatApp DARK
120
-
123
+
124
+ # Ensure the log directory exists in a standard cache location
125
+ log_dir = os.path.expanduser("~/.cache/chat-cli")
126
+ os.makedirs(log_dir, exist_ok=True)
127
+ LOG_FILE = os.path.join(log_dir, "textual.log") # Use absolute path
128
+
121
129
  CSS = """ # Keep SimpleChatApp CSS start
122
130
  #main-content { # Keep SimpleChatApp CSS
123
131
  width: 100%;
@@ -125,6 +133,24 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
125
133
  padding: 0 1;
126
134
  }
127
135
 
136
+ #app-info-bar {
137
+ width: 100%;
138
+ height: 1;
139
+ background: $surface-darken-3;
140
+ color: $text-muted;
141
+ padding: 0 1;
142
+ }
143
+
144
+ #version-info {
145
+ width: auto;
146
+ text-align: left;
147
+ }
148
+
149
+ #model-info {
150
+ width: 1fr;
151
+ text-align: right;
152
+ }
153
+
128
154
  #conversation-title { # Keep SimpleChatApp CSS
129
155
  width: 100%; # Keep SimpleChatApp CSS
130
156
  height: 2;
@@ -135,6 +161,19 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
135
161
  border-bottom: solid $primary-darken-2;
136
162
  }
137
163
 
164
+ #action-buttons {
165
+ width: 100%;
166
+ height: auto;
167
+ padding: 0 1; /* Corrected padding: 0 vertical, 1 horizontal */
168
+ align-horizontal: center;
169
+ background: $surface-darken-1;
170
+ }
171
+
172
+ #new-chat-button, #change-title-button {
173
+ margin: 0 1;
174
+ min-width: 15;
175
+ }
176
+
138
177
  #messages-container { # Keep SimpleChatApp CSS
139
178
  width: 100%; # Keep SimpleChatApp CSS
140
179
  height: 1fr;
@@ -237,21 +276,22 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
237
276
  align: center middle;
238
277
  }
239
278
  """
240
-
279
+
241
280
  BINDINGS = [ # Keep SimpleChatApp BINDINGS, ensure Enter is not globally bound for settings
242
281
  Binding("q", "quit", "Quit", show=True, key_display="q"),
243
- Binding("n", "action_new_conversation", "New Chat", show=True, key_display="n"),
244
- Binding("c", "action_new_conversation", "New Chat", show=False, key_display="c"),
282
+ # Add priority=True to ensure these capture before input
283
+ Binding("n", "action_new_conversation", "New Chat", show=True, key_display="n", priority=True),
284
+ Binding("c", "action_new_conversation", "New Chat", show=False, key_display="c", priority=True), # Add priority to alias too
245
285
  Binding("escape", "escape", "Cancel / Stop", show=True, key_display="esc"), # Escape might close settings panel too
246
286
  Binding("ctrl+c", "quit", "Quit", show=False),
247
- Binding("h", "view_history", "History", show=True, key_display="h"),
248
- Binding("s", "settings", "Settings", show=True, key_display="s"),
249
- Binding("t", "action_update_title", "Update Title", show=True, key_display="t"),
287
+ Binding("h", "view_history", "History", show=True, key_display="h", priority=True), # Add priority
288
+ Binding("s", "settings", "Settings", show=True, key_display="s", priority=True), # Add priority
289
+ Binding("t", "action_update_title", "Update Title", show=True, key_display="t", priority=True), # Add priority
250
290
  ] # Keep SimpleChatApp BINDINGS end
251
-
291
+
252
292
  current_conversation = reactive(None) # Keep SimpleChatApp reactive var
253
293
  is_generating = reactive(False) # Keep SimpleChatApp reactive var
254
-
294
+
255
295
  def __init__(self, initial_text: Optional[str] = None): # Keep SimpleChatApp __init__
256
296
  super().__init__() # Keep SimpleChatApp __init__
257
297
  self.db = ChatDatabase() # Keep SimpleChatApp __init__
@@ -259,27 +299,38 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
259
299
  self.selected_model = CONFIG["default_model"] # Keep SimpleChatApp __init__
260
300
  self.selected_style = CONFIG["default_style"] # Keep SimpleChatApp __init__
261
301
  self.initial_text = initial_text # Keep SimpleChatApp __init__
262
-
302
+ # Removed self.input_widget instance variable
303
+
263
304
  def compose(self) -> ComposeResult: # Modify SimpleChatApp compose
264
305
  """Create the simplified application layout."""
265
306
  yield Header()
266
-
307
+
267
308
  with Vertical(id="main-content"):
309
+ # Add app info bar with version and model info
310
+ with Horizontal(id="app-info-bar"):
311
+ yield Static(f"Chat Console v{__version__}", id="version-info") # Use imported version
312
+ yield Static(f"Model: {self.selected_model}", id="model-info")
313
+
268
314
  # Conversation title
269
315
  yield Static("New Conversation", id="conversation-title")
270
-
316
+
317
+ # Add action buttons at the top for visibility
318
+ with Horizontal(id="action-buttons"):
319
+ yield Button("+ New Chat", id="new-chat-button", variant="success")
320
+ yield Button("✎ Change Title", id="change-title-button", variant="primary")
321
+
271
322
  # Messages area
272
323
  with ScrollableContainer(id="messages-container"):
273
324
  # Will be populated with messages
274
325
  pass
275
-
326
+
276
327
  # Loading indicator
277
328
  yield Static("Generating response...", id="loading-indicator", classes="hidden")
278
-
329
+
279
330
  # Input area
280
331
  with Container(id="input-area"):
281
- yield Input(placeholder="Type your message here...", id="message-input")
282
- # Removed Static widgets previously used for diagnosis
332
+ # Use the custom InputWithFocus widget
333
+ yield InputWithFocus(placeholder="Type your message here...", id="message-input")
283
334
 
284
335
  # --- Add Settings Panel (hidden initially) ---
285
336
  with Container(id="settings-panel"):
@@ -289,18 +340,30 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
289
340
  with Horizontal(id="settings-buttons"):
290
341
  yield Button("Save", id="settings-save-button", variant="success")
291
342
  yield Button("Cancel", id="settings-cancel-button", variant="error")
292
-
343
+
293
344
  yield Footer()
294
-
345
+
295
346
  async def on_mount(self) -> None: # Keep SimpleChatApp on_mount
296
347
  """Initialize the application on mount.""" # Keep SimpleChatApp on_mount docstring
348
+ # Add diagnostic logging for bindings
349
+ print(f"Registered bindings: {self.__class__.BINDINGS}") # Corrected access to class attribute
350
+
351
+ # Update the version display (already imported at top)
352
+ try:
353
+ version_info = self.query_one("#version-info", Static)
354
+ version_info.update(f"Chat Console v{__version__}")
355
+ except Exception:
356
+ pass # Silently ignore if widget not found yet
357
+
358
+ self.update_app_info() # Update the model info
359
+
297
360
  # Check API keys and services # Keep SimpleChatApp on_mount
298
361
  api_issues = [] # Keep SimpleChatApp on_mount
299
362
  if not OPENAI_API_KEY: # Keep SimpleChatApp on_mount
300
363
  api_issues.append("- OPENAI_API_KEY is not set") # Keep SimpleChatApp on_mount
301
364
  if not ANTHROPIC_API_KEY: # Keep SimpleChatApp on_mount
302
365
  api_issues.append("- ANTHROPIC_API_KEY is not set") # Keep SimpleChatApp on_mount
303
-
366
+
304
367
  # Check Ollama availability and try to start if not running # Keep SimpleChatApp on_mount
305
368
  from app.utils import ensure_ollama_running # Keep SimpleChatApp on_mount
306
369
  if not ensure_ollama_running(): # Keep SimpleChatApp on_mount
@@ -315,7 +378,7 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
315
378
  api_issues.append("- No Ollama models found") # Keep SimpleChatApp on_mount
316
379
  except Exception: # Keep SimpleChatApp on_mount
317
380
  api_issues.append("- Error connecting to Ollama server") # Keep SimpleChatApp on_mount
318
-
381
+
319
382
  if api_issues: # Keep SimpleChatApp on_mount
320
383
  self.notify( # Keep SimpleChatApp on_mount
321
384
  "Service issues detected:\n" + "\n".join(api_issues) + # Keep SimpleChatApp on_mount
@@ -324,10 +387,10 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
324
387
  severity="warning", # Keep SimpleChatApp on_mount
325
388
  timeout=10 # Keep SimpleChatApp on_mount
326
389
  ) # Keep SimpleChatApp on_mount
327
-
390
+
328
391
  # Create a new conversation # Keep SimpleChatApp on_mount
329
392
  await self.create_new_conversation() # Keep SimpleChatApp on_mount
330
-
393
+
331
394
  # If initial text was provided, send it # Keep SimpleChatApp on_mount
332
395
  if self.initial_text: # Keep SimpleChatApp on_mount
333
396
  input_widget = self.query_one("#message-input", Input) # Keep SimpleChatApp on_mount
@@ -335,72 +398,105 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
335
398
  await self.action_send_message() # Keep SimpleChatApp on_mount
336
399
  else: # Keep SimpleChatApp on_mount
337
400
  # Focus the input if no initial text # Keep SimpleChatApp on_mount
401
+ # Removed assignment to self.input_widget
338
402
  self.query_one("#message-input").focus() # Keep SimpleChatApp on_mount
339
-
403
+
340
404
  async def create_new_conversation(self) -> None: # Keep SimpleChatApp create_new_conversation
341
405
  """Create a new chat conversation.""" # Keep SimpleChatApp create_new_conversation docstring
406
+ log("Entering create_new_conversation") # Added log
342
407
  # Create new conversation in database using selected model and style # Keep SimpleChatApp create_new_conversation
343
408
  model = self.selected_model # Keep SimpleChatApp create_new_conversation
344
409
  style = self.selected_style # Keep SimpleChatApp create_new_conversation
345
-
410
+
346
411
  # Create a title for the new conversation # Keep SimpleChatApp create_new_conversation
347
412
  title = f"New conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})" # Keep SimpleChatApp create_new_conversation
348
-
413
+
349
414
  # Create conversation in database using the correct method # Keep SimpleChatApp create_new_conversation
415
+ log(f"Creating conversation with title: {title}, model: {model}, style: {style}") # Added log
350
416
  conversation_id = self.db.create_conversation(title, model, style) # Keep SimpleChatApp create_new_conversation
351
-
417
+ log(f"Database returned conversation_id: {conversation_id}") # Added log
418
+
352
419
  # Get the full conversation data # Keep SimpleChatApp create_new_conversation
353
420
  conversation_data = self.db.get_conversation(conversation_id) # Keep SimpleChatApp create_new_conversation
354
-
421
+
355
422
  # Set as current conversation # Keep SimpleChatApp create_new_conversation
356
423
  self.current_conversation = Conversation.from_dict(conversation_data) # Keep SimpleChatApp create_new_conversation
357
-
424
+
358
425
  # Update UI # Keep SimpleChatApp create_new_conversation
359
- title = self.query_one("#conversation-title", Static) # Keep SimpleChatApp create_new_conversation
360
- title.update(self.current_conversation.title) # Keep SimpleChatApp create_new_conversation
361
-
426
+ title_widget = self.query_one("#conversation-title", Static) # Keep SimpleChatApp create_new_conversation
427
+ title_widget.update(self.current_conversation.title) # Keep SimpleChatApp create_new_conversation
428
+
362
429
  # Clear messages and update UI # Keep SimpleChatApp create_new_conversation
363
430
  self.messages = [] # Keep SimpleChatApp create_new_conversation
431
+ log("Finished updating messages UI in create_new_conversation") # Added log
364
432
  await self.update_messages_ui() # Keep SimpleChatApp create_new_conversation
365
-
433
+ self.update_app_info() # Update model info after potentially loading conversation
434
+
366
435
  async def action_new_conversation(self) -> None: # Keep SimpleChatApp action_new_conversation
367
436
  """Handle the new conversation action.""" # Keep SimpleChatApp action_new_conversation docstring
437
+ log("--- ENTERING action_new_conversation ---") # Add entry log
438
+ # Focus check removed - relying on priority=True in binding
439
+
440
+ log("action_new_conversation EXECUTING") # Add execution log
368
441
  await self.create_new_conversation() # Keep SimpleChatApp action_new_conversation
369
-
442
+ log("action_new_conversation finished") # Added log
443
+
370
444
  def action_escape(self) -> None: # Modify SimpleChatApp action_escape
371
445
  """Handle escape key globally."""
446
+ log("action_escape triggered") # Added log
372
447
  settings_panel = self.query_one("#settings-panel")
448
+ log(f"Settings panel visible: {settings_panel.has_class('visible')}") # Added log
373
449
  if settings_panel.has_class("visible"):
450
+ log("Hiding settings panel") # Added log
374
451
  # If settings panel is visible, hide it
375
452
  settings_panel.remove_class("visible")
376
453
  self.query_one("#message-input").focus() # Focus input after closing settings
377
454
  elif self.is_generating:
455
+ log("Stopping generation") # Added log
378
456
  # Otherwise, stop generation if running
379
457
  self.is_generating = False # Keep SimpleChatApp action_escape
380
458
  self.notify("Generation stopped", severity="warning") # Keep SimpleChatApp action_escape
381
459
  loading = self.query_one("#loading-indicator") # Keep SimpleChatApp action_escape
382
460
  loading.add_class("hidden") # Keep SimpleChatApp action_escape
383
- # else: # Optional: Add other escape behavior for the main screen if desired # Keep SimpleChatApp action_escape comment
461
+ else: # Optional: Add other escape behavior for the main screen if desired # Keep SimpleChatApp action_escape comment
462
+ log("Escape pressed, but settings not visible and not generating.") # Added log
384
463
  # pass # Keep SimpleChatApp action_escape comment
385
464
 
386
- # Removed action_confirm_or_send - Enter is handled by Input submission # Keep SimpleChatApp comment
465
+ def update_app_info(self) -> None:
466
+ """Update the displayed app information."""
467
+ try:
468
+ # Update model info
469
+ model_info = self.query_one("#model-info", Static)
470
+ model_display = self.selected_model
471
+
472
+ # Try to get a more readable name from config if available
473
+ if self.selected_model in CONFIG["available_models"]:
474
+ provider = CONFIG["available_models"][self.selected_model]["provider"]
475
+ display_name = CONFIG["available_models"][self.selected_model]["display_name"]
476
+ model_display = f"{display_name} ({provider.capitalize()})"
477
+
478
+ model_info.update(f"Model: {model_display}")
479
+ except Exception as e:
480
+ # Silently handle errors to prevent crashes
481
+ log.error(f"Error updating app info: {e}") # Log error instead of passing silently
482
+ pass
387
483
 
388
484
  async def update_messages_ui(self) -> None: # Keep SimpleChatApp update_messages_ui
389
485
  """Update the messages UI.""" # Keep SimpleChatApp update_messages_ui docstring
390
486
  # Clear existing messages # Keep SimpleChatApp update_messages_ui
391
487
  messages_container = self.query_one("#messages-container") # Keep SimpleChatApp update_messages_ui
392
488
  messages_container.remove_children() # Keep SimpleChatApp update_messages_ui
393
-
489
+
394
490
  # Add messages with a small delay between each # Keep SimpleChatApp update_messages_ui
395
491
  for message in self.messages: # Keep SimpleChatApp update_messages_ui
396
492
  display = MessageDisplay(message, highlight_code=CONFIG["highlight_code"]) # Keep SimpleChatApp update_messages_ui
397
493
  messages_container.mount(display) # Keep SimpleChatApp update_messages_ui
398
494
  messages_container.scroll_end(animate=False) # Keep SimpleChatApp update_messages_ui
399
495
  await asyncio.sleep(0.01) # Small delay to prevent UI freezing # Keep SimpleChatApp update_messages_ui
400
-
496
+
401
497
  # Final scroll to bottom # Keep SimpleChatApp update_messages_ui
402
498
  messages_container.scroll_end(animate=False) # Keep SimpleChatApp update_messages_ui
403
-
499
+
404
500
  async def on_input_submitted(self, event: Input.Submitted) -> None: # Keep SimpleChatApp on_input_submitted
405
501
  """Handle input submission (Enter key in the main input).""" # Keep SimpleChatApp on_input_submitted docstring
406
502
  await self.action_send_message() # Restore direct call # Keep SimpleChatApp on_input_submitted
@@ -409,38 +505,38 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
409
505
  """Initiate message sending.""" # Keep SimpleChatApp action_send_message docstring
410
506
  input_widget = self.query_one("#message-input", Input) # Keep SimpleChatApp action_send_message
411
507
  content = input_widget.value.strip() # Keep SimpleChatApp action_send_message
412
-
508
+
413
509
  if not content or not self.current_conversation: # Keep SimpleChatApp action_send_message
414
510
  return # Keep SimpleChatApp action_send_message
415
-
511
+
416
512
  # Clear input # Keep SimpleChatApp action_send_message
417
513
  input_widget.value = "" # Keep SimpleChatApp action_send_message
418
-
514
+
419
515
  # Create user message # Keep SimpleChatApp action_send_message
420
516
  user_message = Message(role="user", content=content) # Keep SimpleChatApp action_send_message
421
517
  self.messages.append(user_message) # Keep SimpleChatApp action_send_message
422
-
518
+
423
519
  # Save to database # Keep SimpleChatApp action_send_message
424
520
  self.db.add_message( # Keep SimpleChatApp action_send_message
425
521
  self.current_conversation.id, # Keep SimpleChatApp action_send_message
426
522
  "user", # Keep SimpleChatApp action_send_message
427
523
  content # Keep SimpleChatApp action_send_message
428
524
  ) # Keep SimpleChatApp action_send_message
429
-
525
+
430
526
  # Check if this is the first message in the conversation
431
527
  # Note: We check length *before* adding the potential assistant message
432
- is_first_message = len(self.messages) == 1
433
-
528
+ is_first_message = len(self.messages) == 1
529
+
434
530
  # Update UI with user message first
435
531
  await self.update_messages_ui()
436
-
532
+
437
533
  # If this is the first message and dynamic titles are enabled, generate one
438
534
  if is_first_message and self.current_conversation and CONFIG.get("generate_dynamic_titles", True):
439
535
  log("First message detected, generating title...")
440
536
  title_generation_in_progress = True # Use a local flag
441
537
  loading = self.query_one("#loading-indicator")
442
538
  loading.remove_class("hidden") # Show loading for title gen
443
-
539
+
444
540
  try:
445
541
  # Get appropriate client
446
542
  model = self.selected_model
@@ -458,16 +554,16 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
458
554
  self.current_conversation.id,
459
555
  title=title
460
556
  )
461
-
557
+
462
558
  # Update UI title
463
559
  title_widget = self.query_one("#conversation-title", Static)
464
560
  title_widget.update(title)
465
-
561
+
466
562
  # Update conversation object
467
563
  self.current_conversation.title = title
468
-
564
+
469
565
  self.notify(f"Conversation title set to: {title}", severity="information", timeout=3)
470
-
566
+
471
567
  except Exception as e:
472
568
  log.error(f"Failed to generate title: {str(e)}")
473
569
  self.notify(f"Failed to generate title: {str(e)}", severity="warning")
@@ -480,24 +576,25 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
480
576
 
481
577
  # Generate AI response (will set self.is_generating and handle loading indicator)
482
578
  await self.generate_response()
483
-
579
+
484
580
  # Focus back on input
485
581
  input_widget.focus()
486
-
582
+
487
583
  async def generate_response(self) -> None: # Keep SimpleChatApp generate_response
488
584
  """Generate an AI response.""" # Keep SimpleChatApp generate_response docstring
489
585
  if not self.current_conversation or not self.messages: # Keep SimpleChatApp generate_response
490
586
  return # Keep SimpleChatApp generate_response
491
-
587
+
492
588
  self.is_generating = True # Keep SimpleChatApp generate_response
589
+ log(f"Setting is_generating to True") # Added log
493
590
  loading = self.query_one("#loading-indicator") # Keep SimpleChatApp generate_response
494
591
  loading.remove_class("hidden") # Keep SimpleChatApp generate_response
495
-
592
+
496
593
  try: # Keep SimpleChatApp generate_response
497
594
  # Get conversation parameters # Keep SimpleChatApp generate_response
498
595
  model = self.selected_model # Keep SimpleChatApp generate_response
499
596
  style = self.selected_style # Keep SimpleChatApp generate_response
500
-
597
+
501
598
  # Convert messages to API format # Keep SimpleChatApp generate_response
502
599
  api_messages = [] # Keep SimpleChatApp generate_response
503
600
  for msg in self.messages: # Keep SimpleChatApp generate_response
@@ -505,7 +602,7 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
505
602
  "role": msg.role, # Keep SimpleChatApp generate_response
506
603
  "content": msg.content # Keep SimpleChatApp generate_response
507
604
  }) # Keep SimpleChatApp generate_response
508
-
605
+
509
606
  # Get appropriate client # Keep SimpleChatApp generate_response
510
607
  try: # Keep SimpleChatApp generate_response
511
608
  client = BaseModelClient.get_client_for_model(model) # Keep SimpleChatApp generate_response
@@ -514,7 +611,7 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
514
611
  except Exception as e: # Keep SimpleChatApp generate_response
515
612
  self.notify(f"Failed to initialize model client: {str(e)}", severity="error") # Keep SimpleChatApp generate_response
516
613
  return # Keep SimpleChatApp generate_response
517
-
614
+
518
615
  # Start streaming response # Keep SimpleChatApp generate_response
519
616
  assistant_message = Message(role="assistant", content="Thinking...") # Keep SimpleChatApp generate_response
520
617
  self.messages.append(assistant_message) # Keep SimpleChatApp generate_response
@@ -522,23 +619,24 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
522
619
  message_display = MessageDisplay(assistant_message, highlight_code=CONFIG["highlight_code"]) # Keep SimpleChatApp generate_response
523
620
  messages_container.mount(message_display) # Keep SimpleChatApp generate_response
524
621
  messages_container.scroll_end(animate=False) # Keep SimpleChatApp generate_response
525
-
622
+
526
623
  # Add small delay to show thinking state # Keep SimpleChatApp generate_response
527
624
  await asyncio.sleep(0.5) # Keep SimpleChatApp generate_response
528
-
625
+
529
626
  # Stream chunks to the UI with synchronization # Keep SimpleChatApp generate_response
530
627
  update_lock = asyncio.Lock() # Keep SimpleChatApp generate_response
531
-
628
+
532
629
  async def update_ui(content: str): # Keep SimpleChatApp generate_response
533
630
  if not self.is_generating: # Keep SimpleChatApp generate_response
631
+ log("update_ui called but is_generating is False, returning.") # Added log
534
632
  return # Keep SimpleChatApp generate_response
535
-
633
+
536
634
  async with update_lock: # Keep SimpleChatApp generate_response
537
635
  try: # Keep SimpleChatApp generate_response
538
636
  # Clear thinking indicator on first content # Keep SimpleChatApp generate_response
539
637
  if assistant_message.content == "Thinking...": # Keep SimpleChatApp generate_response
540
638
  assistant_message.content = "" # Keep SimpleChatApp generate_response
541
-
639
+
542
640
  # Update message with full content so far # Keep SimpleChatApp generate_response
543
641
  assistant_message.content = content # Keep SimpleChatApp generate_response
544
642
  # Update UI with full content # Keep SimpleChatApp generate_response
@@ -550,14 +648,15 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
550
648
  # Force another refresh to ensure content is visible # Keep SimpleChatApp generate_response
551
649
  self.refresh(layout=True) # Keep SimpleChatApp generate_response
552
650
  except Exception as e: # Keep SimpleChatApp generate_response
553
- logger.error(f"Error updating UI: {str(e)}") # Keep SimpleChatApp generate_response
554
-
651
+ log.error(f"Error updating UI: {str(e)}") # Use log instead of logger
652
+
555
653
  # Generate the response with timeout and cleanup # Keep SimpleChatApp generate_response
556
654
  generation_task = None # Keep SimpleChatApp generate_response
557
655
  try: # Keep SimpleChatApp generate_response
558
656
  # Create a task for the response generation # Keep SimpleChatApp generate_response
559
657
  generation_task = asyncio.create_task( # Keep SimpleChatApp generate_response
560
658
  generate_streaming_response( # Keep SimpleChatApp generate_response
659
+ self, # Pass the app instance
561
660
  api_messages, # Keep SimpleChatApp generate_response
562
661
  model, # Keep SimpleChatApp generate_response
563
662
  style, # Keep SimpleChatApp generate_response
@@ -565,12 +664,13 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
565
664
  update_ui # Keep SimpleChatApp generate_response
566
665
  ) # Keep SimpleChatApp generate_response
567
666
  ) # Keep SimpleChatApp generate_response
568
-
667
+
569
668
  # Wait for response with timeout # Keep SimpleChatApp generate_response
570
669
  full_response = await asyncio.wait_for(generation_task, timeout=60) # Longer timeout # Keep SimpleChatApp generate_response
571
-
670
+
572
671
  # Save to database only if we got a complete response # Keep SimpleChatApp generate_response
573
672
  if self.is_generating and full_response: # Keep SimpleChatApp generate_response
673
+ log("Generation finished, saving full response to DB") # Added log
574
674
  self.db.add_message( # Keep SimpleChatApp generate_response
575
675
  self.current_conversation.id, # Keep SimpleChatApp generate_response
576
676
  "assistant", # Keep SimpleChatApp generate_response
@@ -579,57 +679,72 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
579
679
  # Force a final refresh # Keep SimpleChatApp generate_response
580
680
  self.refresh(layout=True) # Keep SimpleChatApp generate_response
581
681
  await asyncio.sleep(0.1) # Wait for UI to update # Keep SimpleChatApp generate_response
582
-
682
+ elif not full_response:
683
+ log("Generation finished but full_response is empty/None") # Added log
684
+
583
685
  except asyncio.TimeoutError: # Keep SimpleChatApp generate_response
584
- logger.error("Response generation timed out") # Keep SimpleChatApp generate_response
686
+ log.error("Response generation timed out") # Use log instead of logger
585
687
  error_msg = "Response generation timed out. The model may be busy or unresponsive. Please try again." # Keep SimpleChatApp generate_response
586
688
  self.notify(error_msg, severity="error") # Keep SimpleChatApp generate_response
587
-
689
+
588
690
  # Remove the incomplete message # Keep SimpleChatApp generate_response
589
691
  if self.messages and self.messages[-1].role == "assistant": # Keep SimpleChatApp generate_response
590
692
  self.messages.pop() # Keep SimpleChatApp generate_response
591
-
693
+
592
694
  # Update UI to remove the incomplete message # Keep SimpleChatApp generate_response
593
695
  await self.update_messages_ui() # Keep SimpleChatApp generate_response
594
-
696
+
595
697
  finally: # Keep SimpleChatApp generate_response
596
698
  # Ensure task is properly cancelled and cleaned up # Keep SimpleChatApp generate_response
597
699
  if generation_task: # Keep SimpleChatApp generate_response
598
700
  if not generation_task.done(): # Keep SimpleChatApp generate_response
701
+ log("Cancelling generation task") # Added log
599
702
  generation_task.cancel() # Keep SimpleChatApp generate_response
600
703
  try: # Keep SimpleChatApp generate_response
601
704
  await generation_task # Keep SimpleChatApp generate_response
602
705
  except (asyncio.CancelledError, Exception) as e: # Keep SimpleChatApp generate_response
603
- logger.error(f"Error cleaning up generation task: {str(e)}") # Keep SimpleChatApp generate_response
604
-
706
+ log.error(f"Error cleaning up generation task: {str(e)}") # Use log instead of logger
707
+
605
708
  # Force a final UI refresh # Keep SimpleChatApp generate_response
606
709
  self.refresh(layout=True) # Keep SimpleChatApp generate_response
607
-
710
+
608
711
  except Exception as e: # Keep SimpleChatApp generate_response
712
+ log.error(f"Exception during generate_response: {str(e)}") # Added log
609
713
  self.notify(f"Error generating response: {str(e)}", severity="error") # Keep SimpleChatApp generate_response
610
714
  # Add error message # Keep SimpleChatApp generate_response
611
715
  error_msg = f"Error generating response: {str(e)}" # Keep SimpleChatApp generate_response
612
716
  self.messages.append(Message(role="assistant", content=error_msg)) # Keep SimpleChatApp generate_response
613
717
  await self.update_messages_ui() # Keep SimpleChatApp generate_response
614
718
  finally: # Keep SimpleChatApp generate_response
719
+ log(f"Setting is_generating to False in finally block") # Added log
615
720
  self.is_generating = False # Keep SimpleChatApp generate_response
616
721
  loading = self.query_one("#loading-indicator") # Keep SimpleChatApp generate_response
617
722
  loading.add_class("hidden") # Keep SimpleChatApp generate_response
618
-
723
+
619
724
  def on_model_selector_model_selected(self, event: ModelSelector.ModelSelected) -> None: # Keep SimpleChatApp on_model_selector_model_selected
620
725
  """Handle model selection""" # Keep SimpleChatApp on_model_selector_model_selected docstring
621
726
  self.selected_model = event.model_id # Keep SimpleChatApp on_model_selector_model_selected
622
-
727
+ self.update_app_info() # Update the displayed model info
728
+
623
729
  def on_style_selector_style_selected(self, event: StyleSelector.StyleSelected) -> None: # Keep SimpleChatApp on_style_selector_style_selected
624
730
  """Handle style selection""" # Keep SimpleChatApp on_style_selector_style_selected docstring
625
731
  self.selected_style = event.style_id # Keep SimpleChatApp on_style_selector_style_selected
626
-
732
+
627
733
  async def on_button_pressed(self, event: Button.Pressed) -> None: # Modify SimpleChatApp on_button_pressed
628
734
  """Handle button presses."""
629
735
  button_id = event.button.id
630
-
736
+
737
+ if button_id == "new-chat-button":
738
+ # Create a new chat
739
+ await self.create_new_conversation()
740
+ # Focus back on input after creating new chat
741
+ self.query_one("#message-input").focus()
742
+ elif button_id == "change-title-button":
743
+ # Change title
744
+ # Note: action_update_title already checks self.current_conversation
745
+ await self.action_update_title()
631
746
  # --- Handle Settings Panel Buttons ---
632
- if button_id == "settings-cancel-button":
747
+ elif button_id == "settings-cancel-button":
633
748
  settings_panel = self.query_one("#settings-panel")
634
749
  settings_panel.remove_class("visible")
635
750
  self.query_one("#message-input").focus() # Focus input after closing
@@ -639,10 +754,10 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
639
754
  # Get selected values (assuming selectors update self.selected_model/style directly via events)
640
755
  model_to_save = self.selected_model
641
756
  style_to_save = self.selected_style
642
-
757
+
643
758
  # Save globally
644
759
  save_settings_to_config(model_to_save, style_to_save)
645
-
760
+
646
761
  # Update current conversation if one exists
647
762
  if self.current_conversation:
648
763
  self.db.update_conversation(
@@ -660,11 +775,11 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
660
775
  settings_panel = self.query_one("#settings-panel")
661
776
  settings_panel.remove_class("visible")
662
777
  self.query_one("#message-input").focus() # Focus input after closing
663
-
778
+
664
779
  # --- Keep other button logic if needed (currently none) ---
665
780
  # elif button_id == "send-button": # Example if send button existed
666
781
  # await self.action_send_message()
667
-
782
+
668
783
  async def view_chat_history(self) -> None: # Keep SimpleChatApp view_chat_history
669
784
  """Show chat history in a popup.""" # Keep SimpleChatApp view_chat_history docstring
670
785
  # Get recent conversations # Keep SimpleChatApp view_chat_history
@@ -672,32 +787,33 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
672
787
  if not conversations: # Keep SimpleChatApp view_chat_history
673
788
  self.notify("No chat history found", severity="warning") # Keep SimpleChatApp view_chat_history
674
789
  return # Keep SimpleChatApp view_chat_history
675
-
790
+
676
791
  async def handle_selection(selected_id: int) -> None: # Keep SimpleChatApp view_chat_history
677
792
  if not selected_id: # Keep SimpleChatApp view_chat_history
678
793
  return # Keep SimpleChatApp view_chat_history
679
-
794
+
680
795
  # Get full conversation # Keep SimpleChatApp view_chat_history
681
796
  conversation_data = self.db.get_conversation(selected_id) # Keep SimpleChatApp view_chat_history
682
797
  if not conversation_data: # Keep SimpleChatApp view_chat_history
683
798
  self.notify("Could not load conversation", severity="error") # Keep SimpleChatApp view_chat_history
684
799
  return # Keep SimpleChatApp view_chat_history
685
-
800
+
686
801
  # Update current conversation # Keep SimpleChatApp view_chat_history
687
802
  self.current_conversation = Conversation.from_dict(conversation_data) # Keep SimpleChatApp view_chat_history
688
-
803
+
689
804
  # Update title # Keep SimpleChatApp view_chat_history
690
805
  title = self.query_one("#conversation-title", Static) # Keep SimpleChatApp view_chat_history
691
806
  title.update(self.current_conversation.title) # Keep SimpleChatApp view_chat_history
692
-
807
+
693
808
  # Load messages # Keep SimpleChatApp view_chat_history
694
809
  self.messages = [Message(**msg) for msg in self.current_conversation.messages] # Keep SimpleChatApp view_chat_history
695
810
  await self.update_messages_ui() # Keep SimpleChatApp view_chat_history
696
-
811
+
697
812
  # Update model and style selectors # Keep SimpleChatApp view_chat_history
698
813
  self.selected_model = self.current_conversation.model # Keep SimpleChatApp view_chat_history
699
814
  self.selected_style = self.current_conversation.style # Keep SimpleChatApp view_chat_history
700
-
815
+ self.update_app_info() # Update info bar after loading history
816
+
701
817
  self.push_screen(HistoryScreen(conversations, handle_selection)) # Keep SimpleChatApp view_chat_history
702
818
 
703
819
  async def action_view_history(self) -> None: # Keep SimpleChatApp action_view_history
@@ -726,6 +842,11 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
726
842
 
727
843
  async def action_update_title(self) -> None:
728
844
  """Allow users to manually change the conversation title"""
845
+ log("--- ENTERING action_update_title ---") # Add entry log
846
+ # Focus check removed - relying on priority=True in binding
847
+
848
+ log("action_update_title EXECUTING") # Add execution log
849
+
729
850
  if not self.current_conversation:
730
851
  self.notify("No active conversation", severity="warning")
731
852
  return
@@ -129,20 +129,23 @@ class MessageDisplay(RichLog):
129
129
 
130
130
  class InputWithFocus(Input):
131
131
  """Enhanced Input that better handles focus and maintains cursor position"""
132
-
132
+ # Reverted on_key to default Input behavior for 'n' and 't'
133
+ # Let the standard Input handle key presses when focused.
134
+ # We will rely on focus checks within the App's action methods.
135
+
136
+ # Keep custom handling only for Enter submission if needed,
137
+ # but standard Input might already do this. Let's simplify
138
+ # and remove the custom on_key entirely for now unless
133
139
  def on_key(self, event) -> None:
134
- """Custom key handling for input"""
135
- # Let control keys pass through
136
- if event.is_control:
137
- return super().on_key(event)
138
-
139
- # Handle Enter key
140
- if event.key == "enter":
141
- self.post_message(self.Submitted(self))
142
- return
143
-
144
- # Normal input handling for other keys
145
- super().on_key(event)
140
+ # Let global hotkeys 'n' and 't' pass through even when input has focus
141
+ # by simply *not* stopping the event here.
142
+ if event.key == "n" or event.key == "t":
143
+ # Do nothing, allow the event to bubble up to the app level bindings.
144
+ return # Explicitly return to prevent further processing in this method
145
+
146
+ # For all other keys, the event continues to be processed by the Input
147
+ # widget's internal handlers (like _on_key shown in the traceback)
148
+ # because we didn't stop it in this method.
146
149
 
147
150
  class ChatInterface(Container):
148
151
  """Main chat interface container"""
@@ -4,10 +4,14 @@ import time
4
4
  import asyncio
5
5
  import subprocess
6
6
  import logging
7
- from typing import Optional, Dict, Any, List
7
+ from typing import Optional, Dict, Any, List, TYPE_CHECKING
8
8
  from datetime import datetime
9
9
  from .config import CONFIG, save_config
10
10
 
11
+ # Import SimpleChatApp for type hinting only if TYPE_CHECKING is True
12
+ if TYPE_CHECKING:
13
+ from .main import SimpleChatApp
14
+
11
15
  # Set up logging
12
16
  logging.basicConfig(level=logging.INFO)
13
17
  logger = logging.getLogger(__name__)
@@ -74,7 +78,8 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
74
78
  logger.error(f"Failed to generate title after multiple retries. Last error: {last_error}")
75
79
  return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
76
80
 
77
- async def generate_streaming_response(messages: List[Dict], model: str, style: str, client: Any, callback: Any) -> str:
81
+ # Modified signature to accept app instance
82
+ async def generate_streaming_response(app: 'SimpleChatApp', messages: List[Dict], model: str, style: str, client: Any, callback: Any) -> str:
78
83
  """Generate a streaming response from the model"""
79
84
  logger.info(f"Starting streaming response with model: {model}")
80
85
  full_response = ""
@@ -84,6 +89,11 @@ async def generate_streaming_response(messages: List[Dict], model: str, style: s
84
89
 
85
90
  try:
86
91
  async for chunk in client.generate_stream(messages, model, style):
92
+ # Check if generation was cancelled by the app (e.g., via escape key)
93
+ if not app.is_generating:
94
+ logger.info("Generation cancelled by app flag.")
95
+ break # Exit the loop immediately
96
+
87
97
  if chunk: # Only process non-empty chunks
88
98
  buffer.append(chunk)
89
99
  current_time = time.time()
@@ -92,6 +102,10 @@ async def generate_streaming_response(messages: List[Dict], model: str, style: s
92
102
  if current_time - last_update >= update_interval or len(''.join(buffer)) > 100:
93
103
  new_content = ''.join(buffer)
94
104
  full_response += new_content
105
+ # Check again before calling callback, in case it was cancelled during chunk processing
106
+ if not app.is_generating:
107
+ logger.info("Generation cancelled before UI update.")
108
+ break
95
109
  await callback(full_response)
96
110
  buffer = []
97
111
  last_update = current_time
@@ -99,16 +113,22 @@ async def generate_streaming_response(messages: List[Dict], model: str, style: s
99
113
  # Small delay to let UI catch up
100
114
  await asyncio.sleep(0.05)
101
115
 
102
- # Send any remaining content
103
- if buffer:
116
+ # Send any remaining content if generation wasn't cancelled
117
+ if buffer and app.is_generating:
104
118
  new_content = ''.join(buffer)
105
119
  full_response += new_content
106
120
  await callback(full_response)
107
121
 
108
- logger.info("Streaming response completed")
122
+ if app.is_generating:
123
+ logger.info("Streaming response completed normally.")
124
+ else:
125
+ logger.info("Streaming response loop exited due to cancellation.")
126
+
109
127
  return full_response
110
128
  except Exception as e:
111
129
  logger.error(f"Error in streaming response: {str(e)}")
130
+ # Ensure the app knows generation stopped on error
131
+ app.is_generating = False
112
132
  raise
113
133
 
114
134
  def ensure_ollama_running() -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chat-console
3
- Version: 0.1.96.dev1
3
+ Version: 0.2.0
4
4
  Summary: A command-line interface for chatting with LLMs, storing chats and (future) rag interactions
5
5
  Home-page: https://github.com/wazacraftrfid/chat-console
6
6
  Author: Johnathan Greenaway
@@ -14,7 +14,7 @@ with open(os.path.join("app", "__init__.py"), "r", encoding="utf-8") as f:
14
14
 
15
15
  setup(
16
16
  name="chat-console",
17
- version="0.1.96.dev1",
17
+ version="0.2.0",
18
18
  author="Johnathan Greenaway",
19
19
  author_email="john@fimbriata.dev",
20
20
  description="A command-line interface for chatting with LLMs, storing chats and (future) rag interactions",
File without changes