Fixed default model setting. Added ctrl+y to copy latest reply in markdown++
This commit is contained in:
@@ -9,7 +9,7 @@ Author: Rune
|
|||||||
License: MIT
|
License: MIT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "2.1.0"
|
__version__ = "3.0.0-b2"
|
||||||
__author__ = "Rune"
|
__author__ = "Rune"
|
||||||
__license__ = "MIT"
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
|||||||
@@ -863,7 +863,8 @@ class ConfigCommand(Command):
|
|||||||
# Show model selector with search term, same as /model
|
# Show model selector with search term, same as /model
|
||||||
return CommandResult.success(data={"show_model_selector": True, "search": value, "set_as_default": True})
|
return CommandResult.success(data={"show_model_selector": True, "search": value, "set_as_default": True})
|
||||||
else:
|
else:
|
||||||
pass # Show current model, silently ignore
|
# Show model selector without search filter
|
||||||
|
return CommandResult.success(data={"show_model_selector": True, "search": "", "set_as_default": True})
|
||||||
|
|
||||||
elif setting == "system":
|
elif setting == "system":
|
||||||
from oai.constants import DEFAULT_SYSTEM_PROMPT
|
from oai.constants import DEFAULT_SYSTEM_PROMPT
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ from pathlib import Path
|
|||||||
from typing import Set, Dict, Any
|
from typing import Set, Dict, Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
# Import version from single source of truth
|
||||||
|
from oai import __version__
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# APPLICATION METADATA
|
# APPLICATION METADATA
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
APP_NAME = "oAI"
|
APP_NAME = "oAI"
|
||||||
APP_VERSION = "3.0.0"
|
APP_VERSION = __version__ # Single source of truth in oai/__init__.py
|
||||||
APP_URL = "https://iurl.no/oai"
|
APP_URL = "https://iurl.no/oai"
|
||||||
APP_DESCRIPTION = "OpenRouter AI Chat Client with MCP Integration"
|
APP_DESCRIPTION = "OpenRouter AI Chat Client with MCP Integration"
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
"""Main Textual TUI application for oAI."""
|
"""Main Textual TUI application for oAI."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import pyperclip
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import Input
|
from textual.widgets import Input
|
||||||
|
|
||||||
|
from oai import __version__
|
||||||
from oai.commands.registry import CommandStatus, registry
|
from oai.commands.registry import CommandStatus, registry
|
||||||
from oai.config.settings import Settings
|
from oai.config.settings import Settings
|
||||||
from oai.core.client import AIClient
|
from oai.core.client import AIClient
|
||||||
@@ -66,7 +69,7 @@ class oAIChatApp(App):
|
|||||||
"""Compose the TUI layout."""
|
"""Compose the TUI layout."""
|
||||||
model_name = self.session.selected_model.get("name", "") if self.session.selected_model else ""
|
model_name = self.session.selected_model.get("name", "") if self.session.selected_model else ""
|
||||||
model_info = self.session.selected_model if self.session.selected_model else None
|
model_info = self.session.selected_model if self.session.selected_model else None
|
||||||
yield Header(version="3.0.0", model=model_name, model_info=model_info)
|
yield Header(version=__version__, model=model_name, model_info=model_info)
|
||||||
yield ChatDisplay()
|
yield ChatDisplay()
|
||||||
yield InputBar()
|
yield InputBar()
|
||||||
yield CommandDropdown()
|
yield CommandDropdown()
|
||||||
@@ -200,6 +203,10 @@ class oAIChatApp(App):
|
|||||||
elif event.key == "ctrl+n":
|
elif event.key == "ctrl+n":
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
self.call_later(self._handle_next_command)
|
self.call_later(self._handle_next_command)
|
||||||
|
elif event.key in ("f3", "ctrl+y"):
|
||||||
|
# F3 or Ctrl+Y to copy last AI response
|
||||||
|
event.prevent_default()
|
||||||
|
self.action_copy_last_response()
|
||||||
|
|
||||||
def on_input_changed(self, event: Input.Changed) -> None:
|
def on_input_changed(self, event: Input.Changed) -> None:
|
||||||
"""Handle input value changes to show/hide command dropdown."""
|
"""Handle input value changes to show/hide command dropdown."""
|
||||||
@@ -817,11 +824,17 @@ class oAIChatApp(App):
|
|||||||
info_widget = UserMessageWidget(result.message)
|
info_widget = UserMessageWidget(result.message)
|
||||||
await chat_display.add_message(info_widget)
|
await chat_display.add_message(info_widget)
|
||||||
|
|
||||||
|
# Handle special command data (e.g., show_model_selector)
|
||||||
|
if result and result.data:
|
||||||
|
await self._handle_command_data(result.data)
|
||||||
|
|
||||||
async def _handle_command_data(self, data: dict) -> None:
|
async def _handle_command_data(self, data: dict) -> None:
|
||||||
"""Handle special command result data."""
|
"""Handle special command result data."""
|
||||||
# Model selection
|
# Model selection
|
||||||
if "show_model_selector" in data:
|
if "show_model_selector" in data:
|
||||||
self._show_model_selector(data.get("search", ""))
|
search = data.get("search", "")
|
||||||
|
set_as_default = data.get("set_as_default", False)
|
||||||
|
self._show_model_selector(search, set_as_default)
|
||||||
|
|
||||||
# Retry prompt
|
# Retry prompt
|
||||||
elif "retry_prompt" in data:
|
elif "retry_prompt" in data:
|
||||||
@@ -831,7 +844,7 @@ class oAIChatApp(App):
|
|||||||
elif "paste_prompt" in data:
|
elif "paste_prompt" in data:
|
||||||
await self.handle_message(data["paste_prompt"])
|
await self.handle_message(data["paste_prompt"])
|
||||||
|
|
||||||
def _show_model_selector(self, search: str = "") -> None:
|
def _show_model_selector(self, search: str = "", set_as_default: bool = False) -> None:
|
||||||
"""Show the model selector screen."""
|
"""Show the model selector screen."""
|
||||||
def handle_model_selection(selected: Optional[dict]) -> None:
|
def handle_model_selection(selected: Optional[dict]) -> None:
|
||||||
"""Handle the model selection result."""
|
"""Handle the model selection result."""
|
||||||
@@ -840,9 +853,16 @@ class oAIChatApp(App):
|
|||||||
header = self.query_one(Header)
|
header = self.query_one(Header)
|
||||||
header.update_model(selected.get("name", ""), selected)
|
header.update_model(selected.get("name", ""), selected)
|
||||||
|
|
||||||
|
# Save as default if requested
|
||||||
|
if set_as_default:
|
||||||
|
self.settings.set_default_model(selected["id"])
|
||||||
|
|
||||||
# Show confirmation in chat
|
# Show confirmation in chat
|
||||||
async def add_confirmation():
|
async def add_confirmation():
|
||||||
chat_display = self.query_one(ChatDisplay)
|
chat_display = self.query_one(ChatDisplay)
|
||||||
|
if set_as_default:
|
||||||
|
info_widget = UserMessageWidget(f"✓ Default model set to: {selected['id']}")
|
||||||
|
else:
|
||||||
info_widget = UserMessageWidget(f"✓ Model changed to: {selected['id']}")
|
info_widget = UserMessageWidget(f"✓ Model changed to: {selected['id']}")
|
||||||
await chat_display.add_message(info_widget)
|
await chat_display.add_message(info_widget)
|
||||||
|
|
||||||
@@ -1000,3 +1020,36 @@ class oAIChatApp(App):
|
|||||||
async def _handle_next_command(self) -> None:
|
async def _handle_next_command(self) -> None:
|
||||||
"""Handle Ctrl+N to show next message."""
|
"""Handle Ctrl+N to show next message."""
|
||||||
await self.handle_command("/next")
|
await self.handle_command("/next")
|
||||||
|
|
||||||
|
def action_copy_last_response(self) -> None:
|
||||||
|
"""Copy the last AI response to clipboard."""
|
||||||
|
try:
|
||||||
|
chat_display = self.query_one(ChatDisplay)
|
||||||
|
|
||||||
|
# Find the last AssistantMessageWidget
|
||||||
|
assistant_widgets = [
|
||||||
|
child for child in chat_display.children
|
||||||
|
if isinstance(child, AssistantMessageWidget)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not assistant_widgets:
|
||||||
|
self.notify("No AI responses to copy", severity="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the last assistant message
|
||||||
|
last_assistant = assistant_widgets[-1]
|
||||||
|
text = last_assistant.full_text
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
self.notify("Last response is empty", severity="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Copy to clipboard
|
||||||
|
pyperclip.copy(text)
|
||||||
|
|
||||||
|
# Show success notification
|
||||||
|
preview = text[:50] + "..." if len(text) > 50 else text
|
||||||
|
self.notify(f"✓ Copied: {preview}", severity="information")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.notify(f"Copy failed: {e}", severity="error")
|
||||||
|
|||||||
@@ -63,10 +63,12 @@ class HelpScreen(ModalScreen[None]):
|
|||||||
[bold cyan]═══ KEYBOARD SHORTCUTS ═══[/]
|
[bold cyan]═══ KEYBOARD SHORTCUTS ═══[/]
|
||||||
[bold]F1[/] Show this help (Ctrl+H may not work)
|
[bold]F1[/] Show this help (Ctrl+H may not work)
|
||||||
[bold]F2[/] Open model selector (Ctrl+M may not work)
|
[bold]F2[/] Open model selector (Ctrl+M may not work)
|
||||||
|
[bold]F3[/] Copy last AI response to clipboard
|
||||||
[bold]Ctrl+S[/] Show session statistics
|
[bold]Ctrl+S[/] Show session statistics
|
||||||
[bold]Ctrl+L[/] Clear chat display
|
[bold]Ctrl+L[/] Clear chat display
|
||||||
[bold]Ctrl+P[/] Show previous message
|
[bold]Ctrl+P[/] Show previous message
|
||||||
[bold]Ctrl+N[/] Show next message
|
[bold]Ctrl+N[/] Show next message
|
||||||
|
[bold]Ctrl+Y[/] Copy last AI response (alternative to F3)
|
||||||
[bold]Ctrl+Q[/] Quit application
|
[bold]Ctrl+Q[/] Quit application
|
||||||
[bold]Up/Down[/] Navigate input history
|
[bold]Up/Down[/] Navigate input history
|
||||||
[bold]ESC[/] Close dialogs
|
[bold]ESC[/] Close dialogs
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ AssistantMessageWidget {
|
|||||||
#assistant-content {
|
#assistant-content {
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
color: $text;
|
color: #cccccc;
|
||||||
|
link-color: #888888;
|
||||||
|
link-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputBar {
|
InputBar {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Optional, Dict, Any
|
|||||||
class Header(Static):
|
class Header(Static):
|
||||||
"""Header displaying app title, version, current model, and capabilities."""
|
"""Header displaying app title, version, current model, and capabilities."""
|
||||||
|
|
||||||
def __init__(self, version: str = "3.0.0", model: str = "", model_info: Optional[Dict[str, Any]] = None):
|
def __init__(self, version: str = "3.0.1", model: str = "", model_info: Optional[Dict[str, Any]] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.version = version
|
self.version = version
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|||||||
@@ -2,10 +2,29 @@
|
|||||||
|
|
||||||
from typing import Any, AsyncIterator, Tuple
|
from typing import Any, AsyncIterator, Tuple
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.style import Style
|
||||||
|
from rich.theme import Theme
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.widgets import RichLog, Static
|
from textual.widgets import RichLog, Static
|
||||||
|
|
||||||
|
# Custom theme for Markdown rendering - neutral colors matching the dark theme
|
||||||
|
MARKDOWN_THEME = Theme({
|
||||||
|
"markdown.text": Style(color="#cccccc"),
|
||||||
|
"markdown.paragraph": Style(color="#cccccc"),
|
||||||
|
"markdown.code": Style(color="#e0e0e0", bgcolor="#2a2a2a"),
|
||||||
|
"markdown.code_block": Style(color="#e0e0e0", bgcolor="#2a2a2a"),
|
||||||
|
"markdown.heading": Style(color="#ffffff", bold=True),
|
||||||
|
"markdown.h1": Style(color="#ffffff", bold=True),
|
||||||
|
"markdown.h2": Style(color="#eeeeee", bold=True),
|
||||||
|
"markdown.h3": Style(color="#dddddd", bold=True),
|
||||||
|
"markdown.link": Style(color="#aaaaaa", underline=False),
|
||||||
|
"markdown.link_url": Style(color="#888888"),
|
||||||
|
"markdown.emphasis": Style(color="#cccccc", italic=True),
|
||||||
|
"markdown.strong": Style(color="#ffffff", bold=True),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class UserMessageWidget(Static):
|
class UserMessageWidget(Static):
|
||||||
"""Widget for displaying user messages."""
|
"""Widget for displaying user messages."""
|
||||||
@@ -54,7 +73,9 @@ class AssistantMessageWidget(Static):
|
|||||||
if hasattr(chunk, "delta_content") and chunk.delta_content:
|
if hasattr(chunk, "delta_content") and chunk.delta_content:
|
||||||
self.full_text += chunk.delta_content
|
self.full_text += chunk.delta_content
|
||||||
log.clear()
|
log.clear()
|
||||||
log.write(Markdown(self.full_text))
|
# Use neutral code theme for syntax highlighting
|
||||||
|
md = Markdown(self.full_text, code_theme="github-dark", inline_code_theme="github-dark")
|
||||||
|
log.write(md)
|
||||||
|
|
||||||
if hasattr(chunk, "usage") and chunk.usage:
|
if hasattr(chunk, "usage") and chunk.usage:
|
||||||
usage = chunk.usage
|
usage = chunk.usage
|
||||||
@@ -66,4 +87,6 @@ class AssistantMessageWidget(Static):
|
|||||||
self.full_text = content
|
self.full_text = content
|
||||||
log = self.query_one("#assistant-content", RichLog)
|
log = self.query_one("#assistant-content", RichLog)
|
||||||
log.clear()
|
log.clear()
|
||||||
log.write(Markdown(content))
|
# Use neutral code theme for syntax highlighting
|
||||||
|
md = Markdown(content, code_theme="github-dark", inline_code_theme="github-dark")
|
||||||
|
log.write(md)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "oai"
|
name = "oai"
|
||||||
version = "3.0.0"
|
version = "3.0.0-b2" # MUST match oai/__init__.py __version__
|
||||||
description = "OpenRouter AI Chat Client - A feature-rich terminal-based chat application"
|
description = "OpenRouter AI Chat Client - A feature-rich terminal-based chat application"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
Reference in New Issue
Block a user