This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
BasiliskLLM is a wxPython desktop GUI application providing a unified chat interface for multiple LLM providers (Anthropic, OpenAI, Gemini, Mistral, Ollama, DeepSeek, xAI, OpenRouter). It runs on Windows with strong accessibility focus (NVDA screen reader integration). Licensed GPL-2.0, requires Python 3.14.
# Install dependencies
uv sync --frozen --group dev
# Run the application
uv run -m basilisk
# With args: --log-level DEBUG, --minimize, --language <code>, --no-env-account, [bskc_file]
# Run all tests
uv run -m pytest
# Run a single test file
uv run -m pytest tests/test_conversation_profile.py
# Run tests by marker
uv run -m pytest -m "not slow and not integration"
# Lint and format
uv run -m ruff check --fix
uv run -m ruff format
# Build standalone executable
uv run -m cx_Freeze build_exe
# Compile translations
uv run setup.py compile_catalog- Indentation: Tabs (not spaces)
- Line length: 80 characters
- Quote style: Preserve (don't change existing quotes)
- Docstrings: Google style
- Naming:
snake_casefor variables/functions,PascalCasefor classes,UPPER_SNAKE_CASEfor constants - Imports: Standard library -> third-party -> local (
basilisk), alphabetically sorted; addfrom __future__ import annotationsin all presenter and service modules - Translation: Use
_("string")for user-facing text. Translation builtins (_,gettext,ngettext,npgettext,pgettext) are available globally without import - Translator context: Use
# Translators:comment before translatable strings to provide context - Commits: Conventional commits format (commitizen with
cz_conventional_commits)
The codebase follows an MVP (Model-View-Presenter) pattern.
basilisk/__main__.py- CLI entry point, argument parsing, singleton enforcement, IPCbasilisk/main_app.py-MainApp(wx.App)initialization: logging, localization, main frame, IPC, server, auto-updates
Thin wxPython UI layer — layout, event binding, widget access. No business logic.
main_frame.py-MainFrame: root window, notebook tabs, menu bar, status bar, system tray (TaskBarIcon), global hotkeys (Windows)conversation_tab.py-ConversationTab(BaseConversation): chat interface with message history, prompt input, model selection, streaminghistory_msg_text_ctrl.py- Custom text control with NVDA accessibilityprompt_attachments_panel.py- Input area with file attachment supportview_mixins.py-ErrorDisplayMixin:show_error()/show_enhanced_error()for standardised error display- Dialogs: account, preferences, conversation profile, about, update, error display, message editing
Business logic layer. Receive a view via constructor injection. Avoid import wx where possible — delegate wx calls to the view.
main_frame_presenter.py-MainFramePresenter: tab management, file operations, quit flowconversation_presenter.py-ConversationPresenter: completion, streaming, audio recording, draftbase_conversation_presenter.py-BaseConversationPresenter: shared account/model resolutionhistory_presenter.py-HistoryPresenter: message navigation, segment manager, speak-responseaccount_presenter.py-AccountPresenter: account CRUD (inheritsManagerCrudMixin)conversation_profile_presenter.py-ConversationProfilePresenter: profile CRUD (inheritsManagerCrudMixin)edit_block_presenter.py-EditBlockPresenter: message block editing and regenerationconversation_history_presenter.py-ConversationHistoryPresenter: history dialog logicpreferences_presenter.py-PreferencesPresenter: preferences dialog logicattachment_panel_presenter.py-PromptAttachmentPresenter: file attachment handlingsearch_presenter.py-SearchPresenter: in-conversation searchocr_presenter.py-OCRPresenter: screen capture / OCR logicupdate_presenter.py-UpdatePresenter/DownloadPresenter: auto-update flowenhanced_error_presenter.py-EnhancedErrorPresenter: URL detection, clipboard, open-urlpresenter_mixins.py:DestroyGuardMixin+@_guard_destroying— skip callbacks after view is being destroyedManagerCrudMixin— standard add/edit/delete/move for list-based managers
Reusable, thread-safe logic. Instance-based for async (thread + wx.CallAfter callbacks); static methods for pure logic.
conversation_service.py-ConversationService: LLM completion pipelineaccount_model_service.py-AccountModelService: account/model lookup and filteringsearch_service.py-SearchService: text search across conversation historyattachment_service.py-AttachmentService: file attachment processing and resizing
Each LLM provider inherits from BaseEngine (ABC) in base_engine.py. Required implementations:
capabilities: set ofProviderCapability(TEXT, IMAGE, AUDIO, STREAMING)client: cached property returning the provider SDK clientmodels: cached property returninglist[ProviderAIModel]completion(),prepare_message_request(),prepare_message_response()completion_response_with_stream(),completion_response_without_stream()
Pydantic-based settings with YAML persistence (stored via platformdirs):
main_config.py-BasiliskConfig: general, conversation, images, recordings, server, network settingsaccount_config.py-AccountManager: per-provider accounts withSecretStrAPI keys (Keyring/Windows Credential Manager)conversation_profile.py-ConversationProfile: per-conversation defaults (account, model, system prompt, temperature, etc.)- Base class
BasiliskBaseSettingshandles YAML load/save. Loading order: YAML -> env vars -> defaults
conversation_model.py- Pydantic models:Conversation>MessageBlock(request/response pair) >Message+ attachmentsconversation_helper.py- BSKC file format (JSON-based) I/O with version migrationsattached_file.py-AttachmentFileandImageFilewith base64 encoding and auto-resize
- Event binding:
self.Bind(wx.EVT_*, self.on_*) - Dialog lifecycle: always call
dialog.Destroy()afterShowModal() - Layout: use
BoxSizerfor responsive layouts, avoid fixed positioning - Accessibility: proper labels and NVDA integration on all controls
- No wx in presenters: use
view.show_error()/view.show_enhanced_error()(fromErrorDisplayMixin) instead ofwx.MessageBox
singleton_instance/- Single-instance enforcement (Windows mutex, POSIX file lock)ipc/- Inter-process communication (Windows named pipes, POSIX file watcher) for focus and file-open signalscompletion_handler.py- LLM request processing and streamingmessage_segment_manager.py- Message text segmentation for accessible outputsound_manager.py- Audio notification playbackupdater.py- Auto-update with release channels (stable, beta, dev)provider.py- Provider registry and factorydecorators.py-ensure_no_task_running,measure_time,require_list_selection(widget_attr)
ConversationTab._is_destroying = False— set toTrueincleanup_resources()cleanup_resources()callspresenter.cleanup()andocr_handler.cleanup()ConversationPresenter.cleanup(): stops completion (skip_callbacks=True), aborts recording, stops sound, flushes draftHistoryPresenter.cleanup(): destroys_search_dialog, resets_search_presentertoNoneMainFramePresenter.flush_and_save_on_quit()andclose_conversation()calltab.cleanup_resources()- Presenter callbacks that may fire after destruction are decorated with
@_guard_destroying
- Global fixtures in
tests/conftest.py:ai_model,user_message,assistant_message,system_message,message_block,message_block_with_response,empty_conversation,text_file,image_file,attachment,image_attachment,segment_manager; autousemock_display_error_msgandmock_settings_sources - Presenter fixtures in
tests/presenters/conftest.py:base_mock_view(_is_destroying=False),mock_account,mock_model,mock_engine - pytest-mock: Use
mocker.patch()/mocker.patch.object()— prefer over@patchdecorators andwith patch()context managers - Parametrize: Use tuple syntax
("a", "b")not string"a,b"for multi-parameter marks (PT006) - Subtests: Use native pytest 9
subtestsfixture withwith subtests.test(label=x): - Mock views for
ConversationPresentermust setview._is_destroying = Falseexplicitly - Do not use bare
_for tuple unpacking — it shadows the translation builtin and breaks_("Error")calls - Markers:
slow,integration