Skip to content

feat: add mako template rendering#1082

Open
clementb49 wants to merge 13 commits intomasterfrom
feature/mako-templating
Open

feat: add mako template rendering#1082
clementb49 wants to merge 13 commits intomasterfrom
feature/mako-templating

Conversation

@clementb49
Copy link
Collaborator

@clementb49 clementb49 commented Mar 8, 2026

Summary by CodeRabbit

  • New Features

    • Export conversations as HTML files.
    • Preview system prompts before applying.
    • Customize HTML templates for messages and conversation exports.
    • Configure template paths in Preferences with Browse and Write Default template buttons.
  • Bug Fixes / UX

    • Improved error reporting for template rendering and export failures; falls back to default templates when custom ones fail.
  • Chores

    • Added template rendering engine dependency (Mako).
  • Tests

    • Added tests covering template rendering, export, and preference save error handling.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

Walkthrough

Adds a Mako-based TemplateService, configuration and UI for HTML templates, rendering of system prompts and conversation exports to HTML, template-path management in preferences, and tests exercising rendering and export/error paths.

Changes

Cohort / File(s) Summary
Repository config
/.gitignore
Added .worktrees/ ignore pattern.
Config & globals
basilisk/config/__init__.py, basilisk/config/main_config.py, basilisk/global_vars.py
Exported TemplatesSettings, added TemplatesSettings model and templates field to BasiliskConfig, and introduced templates_path global variable.
Template service
basilisk/services/template_service.py
New Mako-based TemplateService with helpers and static methods: render_prompt, render_system_prompt, render_html_message, render_conversation_export; error handling and fallback to default templates.
Presenters
basilisk/presenters/...
basilisk/presenters/base_conversation_presenter.py, .../conversation_presenter.py, .../conversation_profile_presenter.py, .../main_frame_presenter.py, .../preferences_presenter.py
Added BaseConversationPresenter.render_system_prompt, ConversationPresenter.export_to_html, EditConversationProfilePresenter.preview_system_prompt; removed MainFramePresenter.save_conversation_as; persisted template paths in preferences with error handling.
Views / UI
basilisk/views/...
basilisk/views/base_conversation.py, .../conversation_profile_dialog.py, .../main_frame.py, .../html_view_window.py, .../preferences_dialog.py, .../conversation_tab.py
Integrated TemplateService rendering for system prompts and HTML views, added Preview button and handler, new Export menu item and handler, TemplatePathWidget for selecting/writing templates, delegated save-as/export dialogs to tabs.
Tests
tests/presenters/*, tests/services/test_template_service.py
Added tests for render_system_prompt, TemplateService behaviors (success/failure/fallback), conversation export (write path and error cases); removed tests for removed save_conversation_as behavior.
Dependencies
pyproject.toml
Added dependency mako>=1.3.10.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MainFrame
    participant ConversationTab
    participant ConversationPresenter
    participant TemplateService
    participant FileSystem

    User->>MainFrame: Click "Export conversation as HTML"
    MainFrame->>ConversationTab: ask_export_html()
    ConversationTab->>MainFrame: return path
    MainFrame->>ConversationPresenter: export_to_html(path)
    ConversationPresenter->>TemplateService: render_conversation_export(conversation, profile, template_path)
    TemplateService->>FileSystem: load custom template (if provided)
    FileSystem-->>TemplateService: template content / error
    TemplateService->>TemplateService: render Mako template with context
    TemplateService-->>ConversationPresenter: rendered HTML
    ConversationPresenter->>FileSystem: write HTML file
    FileSystem-->>ConversationPresenter: success / OSError
    ConversationPresenter-->>MainFrame: success / show error
Loading
sequenceDiagram
    participant User
    participant View
    participant BaseConversationPresenter
    participant TemplateService

    User->>View: Apply conversation profile
    View->>BaseConversationPresenter: render_system_prompt(profile, account, model)
    BaseConversationPresenter->>TemplateService: render_system_prompt(template_str, profile, account, model)
    TemplateService->>TemplateService: build context (now, profile, account, model)
    TemplateService->>TemplateService: render Mako template
    TemplateService-->>BaseConversationPresenter: rendered prompt
    BaseConversationPresenter-->>View: rendered prompt string
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • AAClause
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add mako template rendering' accurately and concisely describes the main change—introducing Mako-based template rendering functionality throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 98.57% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/mako-templating

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@basilisk/presenters/base_conversation_presenter.py`:
- Around line 88-93: The public method render_system_prompt has an untyped
parameter model; add an explicit type annotation (e.g., model: config.Model) to
the signature and update its Google-style docstring to include the model
parameter type and description so the presenter API remains fully typed and
analyzable; ensure you modify the function signature in
base_conversation_presenter.RenderSystemPrompt and update the docstring for the
model param accordingly.

In `@basilisk/presenters/conversation_presenter.py`:
- Around line 153-167: The TemplateService.render_conversation_export call can
raise (e.g., on a broken template) before the file open, so move the render and
file write into a single try block and catch OSError and ValueError together;
specifically, wrap the
TemplateService.render_conversation_export(self.conversation, profile,
template_path) and the with open(path, "w", encoding="utf-8") as f:
f.write(html) sequence in one try/except and on exception call
self.view.show_error(...) as currently done to surface the error instead of
crashing the menu handler.

In `@basilisk/presenters/conversation_profile_presenter.py`:
- Around line 51-58: The preview is using self.profile (possibly None or stale)
when calling TemplateService.render_system_prompt; instead, gather the current
widget values from the view and construct a temporary profile object (matching
the shape used by validate_and_build_profile) before calling
render_system_prompt so the preview reflects the current form state; replace the
direct use of self.profile in the TemplateService.render_system_prompt call with
this temporary profile built from the view's fields (e.g., values used by
validate_and_build_profile) and keep validate_and_build_profile unchanged.

In `@basilisk/presenters/main_frame_presenter.py`:
- Around line 280-284: The wildcard string passed to the save dialog is not
marked for translation; wrap the file-type filter value "HTML files
(*.html)|*.html" with _(...) and add a translator comment above it (matching the
existing pattern used for the dialog title) so translators see context; update
the call where wildcard=... (alongside defaultFile and style in the same
FileDialog invocation in main_frame_presenter.py) to use _("HTML files
(*.html)|*.html") and add a "# Translators:" line before it.
- Around line 272-288: The presenter method export_conversation currently
constructs a wx.FileDialog and uses a non-localized wildcard; move the
FileDialog logic into the view and have the view return the chosen path to the
presenter: in export_conversation (presenter) get the current tab and, if
present, call a new view method (e.g., view.prompt_export_path(...) or reuse an
existing view API) to obtain the file path, then call
tab.presenter.export_to_html(path) only when a path is returned; remove any
direct wx usage/import from main_frame_presenter.export_conversation. Also
update the wildcard to be localized as _("HTML files") + " (*.html)|*.html" and
add a "# Translators:" comment above that localized wildcard in the view code
where the FileDialog is created.

In `@basilisk/services/template_service.py`:
- Around line 43-65: The _render_inline function currently uses Mako (Template)
which executes arbitrary Python and must not be used to render untrusted
profile.system_prompt content; replace usage by implementing a non-executable
interpolator (e.g., a simple safe string interpolator) and call that for system
prompts instead of _render_inline, keep Mako-only for trusted file-based
templates, and update all callers that render profile.system_prompt (including
the code paths referenced around lines 43-65 and 92-118) to use the new safe
renderer; ensure _render_inline remains for trusted template files or is renamed
to make its executable nature explicit.
- Around line 167-173: The template uses ngettext and pgettext but they are not
guaranteed to be installed into builtins; either import ngettext and pgettext
explicitly in the module that defines ctx (so add imports for ngettext and
pgettext next to _), or update the localization module where
translation.install(...) is called to pass
names=['ngettext','pgettext','npgettext'] so those functions are installed into
builtins; locate the call to translation.install and add the names parameter or
add the explicit imports in the template_service module and ensure ctx includes
the imported names.

In `@basilisk/views/base_conversation.py`:
- Around line 462-468: The system prompt is rendered using self.current_account
and self.current_model before applying the profile's resolved account/model, so
templates referencing account/model get stale context; move the call to
base_conv_presenter.render_system_prompt (and the subsequent
self.system_prompt_txt.SetValue(...)) to after
set_account_and_model_from_profile(profile, fall_back_default_account) or re-run
the render immediately after that call so render_system_prompt uses the updated
current_account/current_model from the profile.
- Around line 462-465: The current code calls
base_conv_presenter.render_system_prompt(profile, self.current_account,
self.current_model) and sets system_prompt_txt before updating the
profile-backed context, and it uses Mako-style rendering which allows arbitrary
Python execution; fix by moving the render call to after the code that updates
self.current_account and self.current_model from profile so the template sees
the new profile context, and replace or constrain Mako rendering in
base_conv_presenter.render_system_prompt to a safe templating approach (e.g., a
sandboxed/limited template engine or explicit variable substitution that forbids
execution of expressions) or sanitize profile.system_prompt before rendering to
strip/escape executable tags; ensure system_prompt_txt.SetValue receives only
the sanitized/safer-rendered output.

In `@basilisk/views/html_view_window.py`:
- Around line 61-64: The HtmlViewWindow constructor calls
TemplateService.render_html_message with
config.conf().templates.html_message_template_path and does not handle template
render errors; wrap the render_html_message call in a try/except around
TemplateService.render_html_message (or wherever render is invoked), catch
exceptions from Mako/runtime, log the error, and then fall back to the embedded
default template (or a bundled default template path) to produce content so a
bad html_message_template_path does not prevent the viewer from opening; ensure
the fallback path or default template is referenced explicitly and that the
exception is surfaced as a controlled log/error message rather than letting it
abort construction.

In `@basilisk/views/preferences_dialog.py`:
- Around line 83-98: The _on_write_default event handler must catch filesystem
errors from shutil.copy and show them via the view error UI instead of letting
exceptions escape; wrap the copy of global_vars.templates_path /
self._default_template_filename to dest in a try/except OSError block inside
_on_write_default, call the standardized error display method provided by
ErrorDisplayMixin (ensure the view class inherits ErrorDisplayMixin from
basilisk/views/view_mixins.py) to report the exception message to the user, and
only call self._path_ctrl.SetValue(str(dest)) on successful copy.

In `@tests/presenters/test_conversation_export.py`:
- Around line 15-18: The mock_view fixture does not set the view lifecycle flag,
so explicitly set conversation_view_base._is_destroying = False inside the
mock_view function before returning it; keep the existing
conversation_view_base.current_profile = None assignment and return
conversation_view_base so tests that instantiate ConversationPresenter have a
deterministic non-destroying view.

In `@tests/services/test_template_service.py`:
- Around line 28-40: The tests permit arbitrary Python execution via
TemplateService.render_prompt by supporting <% ... %> blocks and imports (see
tests test_python_block_execution and test_import_in_block); to fix, remove or
disable interpretation of <% %> execution in TemplateService.render_prompt (or
add a strict non-executable mode by default) so templates are treated as
data-only interpolation, and update/remove tests that assert code
execution/import behavior; if retaining blocks for trusted-local-only use, gate
that behavior behind an explicit opt-in flag (e.g.,
render_prompt(trust_templates=True)) and ensure imports are blocked or sandboxed
unless the opt-in is set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e5162d41-156f-4eee-b083-32d82e962fc4

📥 Commits

Reviewing files that changed from the base of the PR and between 480192f and 0e5cd9f.

⛔ Files ignored due to path filters (3)
  • basilisk/res/templates/conversation_export.mako is excluded by !basilisk/res/**
  • basilisk/res/templates/html_message.mako is excluded by !basilisk/res/**
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .gitignore
  • basilisk/config/__init__.py
  • basilisk/config/main_config.py
  • basilisk/global_vars.py
  • basilisk/presenters/base_conversation_presenter.py
  • basilisk/presenters/conversation_presenter.py
  • basilisk/presenters/conversation_profile_presenter.py
  • basilisk/presenters/main_frame_presenter.py
  • basilisk/presenters/preferences_presenter.py
  • basilisk/services/template_service.py
  • basilisk/views/base_conversation.py
  • basilisk/views/conversation_profile_dialog.py
  • basilisk/views/html_view_window.py
  • basilisk/views/main_frame.py
  • basilisk/views/preferences_dialog.py
  • pyproject.toml
  • tests/presenters/test_base_conversation_presenter.py
  • tests/presenters/test_conversation_export.py
  • tests/services/test_template_service.py

clementb49 and others added 2 commits March 10, 2026 04:06
- Add _ask_save_path() helper to ConversationTab to factorize wx.FileDialog
  for save operations (wildcard, defaultFile, FD_SAVE | FD_OVERWRITE_PROMPT)
- Add ask_save_as() and ask_export_html() to ConversationTab; MainFrame
  delegates to these instead of owning the dialogs
- Remove save_conversation_as() and export_conversation() from
  MainFramePresenter (wx.FileDialog no longer in presenter layer)
- Simplify save_current_conversation() and on_save_as_conversation() now
  that current_tab is always non-None

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- base_conversation_presenter: annotate model param as ProviderAIModel | None
- conversation_presenter: wrap render + file write in single try/except
  (OSError, ValueError) so broken templates surface via show_error
- conversation_profile_presenter: preview_system_prompt builds profile from
  current form state via validate_and_build_profile() instead of using the
  possibly-None/stale self.profile
- base_conversation.py: render system prompt after set_account_and_model_from_profile
  so template context reflects the profile's resolved account/model
- preferences_dialog: TemplatePathWidget becomes wx.Panel + ErrorDisplayMixin;
  PreferencesDialog inherits ErrorDisplayMixin; _on_write_default catches
  OSError and calls show_error() instead of crashing
- preferences_presenter: wrap conf.save() in try/except (OSError,
  ValidationError) and show error without closing dialog on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
basilisk/presenters/conversation_profile_presenter.py (1)

57-64: ⚠️ Potential issue | 🟠 Major

Keep preview rendering side-effect free.

validate_and_build_profile() writes back into self.profile on Lines 82-125, so clicking Preview can mutate the live profile before Save/Cancel. The or self.profile fallback also reuses stale data when Line 79 returns early on a blank name. Please render against a temporary/copy-on-write profile built from the current widget state instead of reusing self.profile.

Suggested direction
-		preview_profile = self.validate_and_build_profile() or self.profile
+		preview_profile = self._build_preview_profile()
def _build_preview_profile(self) -> ConversationProfile:
	preview_profile = (
		self.profile.model_copy(deep=True)
		if self.profile is not None
		else ConversationProfile.model_construct()
	)
	preview_profile.name = self.view.profile_name_txt.GetValue()
	preview_profile.system_prompt = self.view.system_prompt_txt.GetValue()
	# copy the remaining fields from the current widget state here
	return preview_profile
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@basilisk/presenters/conversation_profile_presenter.py` around lines 57 - 64,
The preview flow mutates self.profile because validate_and_build_profile()
writes back to it and the current code falls back to self.profile; instead build
a temporary preview profile from the widget state and pass that into
TemplateService.render_system_prompt so Preview is side-effect free. Implement a
helper like _build_preview_profile() that creates a deep copy (or new
model_construct) of ConversationProfile, populate fields from the view widgets
(e.g. view.profile_name_txt.GetValue(), view.system_prompt_txt.GetValue(), and
other widget values), then replace the call site in render path (where
validate_and_build_profile() and self.profile are used) to use this temporary
profile; keep TemplateService.render_system_prompt(template_str,
preview_profile, self.view.current_account, self.view.current_model) unchanged.
tests/presenters/test_conversation_export.py (1)

15-18: ⚠️ Potential issue | 🟡 Minor

Set _is_destroying = False on the mock view fixture.

These tests instantiate ConversationPresenter, so the fixture should define the lifecycle flag explicitly instead of relying on whatever conversation_view_base happens to provide.

Proposed fix
 `@pytest.fixture`
 def mock_view(conversation_view_base):
 	"""Return a mock view with current_profile set to None."""
 	conversation_view_base.current_profile = None
+	conversation_view_base._is_destroying = False
 	return conversation_view_base

Based on learnings: Mock views for ConversationPresenter must set view._is_destroying = False explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/presenters/test_conversation_export.py` around lines 15 - 18, The mock
view fixture function mock_view should explicitly set the lifecycle flag on the
provided conversation_view_base by assigning
conversation_view_base._is_destroying = False before returning it so that tests
instantiating ConversationPresenter get a deterministic non-destroying view;
update the mock_view function to set current_profile = None and also set
_is_destroying = False on conversation_view_base before returning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@basilisk/presenters/preferences_presenter.py`:
- Around line 122-138: The code mutates the live config (conf) before calling
conf.save(), so on save failure the runtime config is left changed; fix by
staging edits on a copy or restoring the previous state: capture the existing
values from config.conf() (or deep-copy the conf object) before assigning the
new paths from self.view.html_message_template_path.get_path() and
self.view.html_export_template_path.get_path(), then call conf.save() on the
modified copy (or apply assignments to the real conf only after successful
save); if save raises in the except block, restore the original values back into
the live config and then call self.view.show_error(...) so runtime state remains
unchanged on failure.

In `@basilisk/views/base_conversation.py`:
- Around line 465-468: The code currently overwrites the editor control with the
rendered template by calling self.system_prompt_txt.SetValue(rendered) after
base_conv_presenter.render_system_prompt(...); instead, leave the editor showing
the raw template (profile.system_prompt) so previews can re-render; remove or
replace the SetValue(rendered) line and ensure rendering is only done in
preview/send/export code paths (where base_conv_presenter.render_system_prompt
is invoked), and if needed set the control to profile.system_prompt (not
rendered) so system_prompt_txt continues to expose the original Mako template
used by conversation_profile_presenter.

In `@basilisk/views/conversation_tab.py`:
- Around line 630-638: The default export filename in ask_export_html uses
self.GetLabel() (panel label) which often yields "conversation.html"; change it
to use the conversation's title (prefer self.conversation.title, falling back to
self.title or a localized "conversation") when building default, i.e. update the
default variable assignment in ask_export_html to derive the base name from
self.conversation.title/self.title and then append ".html", ensuring proper
fallback and localization.

In `@basilisk/views/preferences_dialog.py`:
- Around line 75-80: The file dialog wildcard contains hard-coded English
labels; wrap the file-filter labels inside the wildcard in _(...) and add
translator context comments so they are localizable: update the wx.FileDialog
wildcard entries used in the preferences dialog (the call to wx.FileDialog) to
build the wildcard string with _("Mako files (*.mako)|*.mako") and _("All files
(*.*)|*.*") (or equivalent localized pieces) and place preceding "#
Translators:" comments describing each label so translators have context; do the
same for the second wildcard usage later in the same function/block.

In `@tests/presenters/test_preferences_presenter.py`:
- Around line 141-178: The tests
test_oserror_on_save_shows_error_and_keeps_dialog_open and
test_validation_error_on_save_shows_error_and_keeps_dialog_open should
explicitly set mock_view.html_message_template_path and
mock_view.html_export_template_path to mocks that have get_path.return_value
(e.g., MagicMock(get_path=MagicMock(return_value="..."))) so presenter.on_ok()
calls to html_message_template_path.get_path() and
html_export_template_path.get_path() are exercised before mock_conf.save() is
invoked; update both tests to assign those attributes on mock_view prior to
calling presenter.on_ok() so autovivification does not hide interface
regressions.

---

Duplicate comments:
In `@basilisk/presenters/conversation_profile_presenter.py`:
- Around line 57-64: The preview flow mutates self.profile because
validate_and_build_profile() writes back to it and the current code falls back
to self.profile; instead build a temporary preview profile from the widget state
and pass that into TemplateService.render_system_prompt so Preview is
side-effect free. Implement a helper like _build_preview_profile() that creates
a deep copy (or new model_construct) of ConversationProfile, populate fields
from the view widgets (e.g. view.profile_name_txt.GetValue(),
view.system_prompt_txt.GetValue(), and other widget values), then replace the
call site in render path (where validate_and_build_profile() and self.profile
are used) to use this temporary profile; keep
TemplateService.render_system_prompt(template_str, preview_profile,
self.view.current_account, self.view.current_model) unchanged.

In `@tests/presenters/test_conversation_export.py`:
- Around line 15-18: The mock view fixture function mock_view should explicitly
set the lifecycle flag on the provided conversation_view_base by assigning
conversation_view_base._is_destroying = False before returning it so that tests
instantiating ConversationPresenter get a deterministic non-destroying view;
update the mock_view function to set current_profile = None and also set
_is_destroying = False on conversation_view_base before returning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4dc7814e-8080-4d54-a68a-9ca9425a9787

📥 Commits

Reviewing files that changed from the base of the PR and between 0e5cd9f and 64d06db.

📒 Files selected for processing (12)
  • basilisk/presenters/base_conversation_presenter.py
  • basilisk/presenters/conversation_presenter.py
  • basilisk/presenters/conversation_profile_presenter.py
  • basilisk/presenters/main_frame_presenter.py
  • basilisk/presenters/preferences_presenter.py
  • basilisk/views/base_conversation.py
  • basilisk/views/conversation_tab.py
  • basilisk/views/main_frame.py
  • basilisk/views/preferences_dialog.py
  • tests/presenters/test_conversation_export.py
  • tests/presenters/test_main_frame_presenter.py
  • tests/presenters/test_preferences_presenter.py
💤 Files with no reviewable changes (2)
  • tests/presenters/test_main_frame_presenter.py
  • basilisk/presenters/main_frame_presenter.py

Comment on lines +122 to +138
conf.templates.html_message_template_path = (
self.view.html_message_template_path.get_path()
)
conf.templates.html_export_template_path = (
self.view.html_export_template_path.get_path()
)

conf.save()
try:
conf.save()
except (OSError, ValidationError) as exc:
self.view.show_error(
# Translators: Error shown when saving preferences fails
_("Failed to save preferences: {error}").format(error=exc),
# Translators: Title for the preferences save error dialog
_("Save error"),
)
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Roll back staged config changes when saving fails.

These assignments mutate the live config.conf() object before conf.save() runs. If save() raises, the dialog stays open but the process is still using the unsaved values, so a failed save can silently change runtime behavior for the rest of the session. Stage the edits on a copy or restore the previous config state in the except path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@basilisk/presenters/preferences_presenter.py` around lines 122 - 138, The
code mutates the live config (conf) before calling conf.save(), so on save
failure the runtime config is left changed; fix by staging edits on a copy or
restoring the previous state: capture the existing values from config.conf() (or
deep-copy the conf object) before assigning the new paths from
self.view.html_message_template_path.get_path() and
self.view.html_export_template_path.get_path(), then call conf.save() on the
modified copy (or apply assignments to the real conf only after successful
save); if save raises in the except block, restore the original values back into
the live config and then call self.view.show_error(...) so runtime state remains
unchanged on failure.

Comment on lines +465 to +468
rendered = self.base_conv_presenter.render_system_prompt(
profile, self.current_account, self.current_model
)
self.system_prompt_txt.SetValue(rendered)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep system_prompt_txt on the raw template.

basilisk/presenters/conversation_profile_presenter.py:50-75 reads self.view.system_prompt_txt.GetValue() as the template source for preview rendering. Writing rendered back into this control here strips the Mako syntax as soon as apply_profile() runs, so subsequent previews cannot re-render against the current profile/account/model state. Render only in preview/send/export paths, and leave the editor bound to profile.system_prompt.

Suggested fix
-		rendered = self.base_conv_presenter.render_system_prompt(
-			profile, self.current_account, self.current_model
-		)
-		self.system_prompt_txt.SetValue(rendered)
+		self.system_prompt_txt.SetValue(profile.system_prompt or "")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rendered = self.base_conv_presenter.render_system_prompt(
profile, self.current_account, self.current_model
)
self.system_prompt_txt.SetValue(rendered)
self.system_prompt_txt.SetValue(profile.system_prompt or "")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@basilisk/views/base_conversation.py` around lines 465 - 468, The code
currently overwrites the editor control with the rendered template by calling
self.system_prompt_txt.SetValue(rendered) after
base_conv_presenter.render_system_prompt(...); instead, leave the editor showing
the raw template (profile.system_prompt) so previews can re-render; remove or
replace the SetValue(rendered) line and ensure rendering is only done in
preview/send/export code paths (where base_conv_presenter.render_system_prompt
is invoked), and if needed set the control to profile.system_prompt (not
rendered) so system_prompt_txt continues to expose the original Mako template
used by conversation_profile_presenter.

Comment on lines +630 to +638
def ask_export_html(self) -> None:
"""Show a save dialog for .html and export the conversation."""
default = f"{self.GetLabel() or _('conversation')}.html"
path = self._ask_save_path(
# Translators: Dialog title when exporting a conversation to HTML
_("Export conversation as HTML"),
"HTML files (*.html)|*.html",
default_file=default,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the conversation title for the default export filename.

self.GetLabel() is the panel label, not the notebook tab title, so this will usually fall back to "conversation.html" even when the tab already has a meaningful title. Derive the suggestion from self.conversation.title or self.title instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@basilisk/views/conversation_tab.py` around lines 630 - 638, The default
export filename in ask_export_html uses self.GetLabel() (panel label) which
often yields "conversation.html"; change it to use the conversation's title
(prefer self.conversation.title, falling back to self.title or a localized
"conversation") when building default, i.e. update the default variable
assignment in ask_export_html to derive the base name from
self.conversation.title/self.title and then append ".html", ensuring proper
fallback and localization.

Comment on lines +75 to +80
with wx.FileDialog(
self,
# Translators: Dialog title when browsing for a template file
_("Select template file"),
wildcard="Mako files (*.mako)|*.mako|All files (*.*)|*.*",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize the file-filter labels in these dialogs.

"Mako files" and "All files" are user-visible here, but they are hard-coded English strings inside the wildcard. Please wrap those labels in _() and add # Translators: comments so the new template dialogs stay consistent with the rest of the localized UI.

As per coding guidelines, "Use _(\"string\") for all user-facing text to enable translation" and "Use # Translators: comment before translatable strings to provide context for translators".

Also applies to: 87-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@basilisk/views/preferences_dialog.py` around lines 75 - 80, The file dialog
wildcard contains hard-coded English labels; wrap the file-filter labels inside
the wildcard in _(...) and add translator context comments so they are
localizable: update the wx.FileDialog wildcard entries used in the preferences
dialog (the call to wx.FileDialog) to build the wildcard string with _("Mako
files (*.mako)|*.mako") and _("All files (*.*)|*.*") (or equivalent localized
pieces) and place preceding "# Translators:" comments describing each label so
translators have context; do the same for the second wildcard usage later in the
same function/block.

Comment on lines +141 to +178
def test_oserror_on_save_shows_error_and_keeps_dialog_open(
self, mock_view, make_presenter, mocker
):
"""OSError from conf.save() shows error and does not close the dialog."""
mock_wx = MagicMock()
mocker.patch.dict(sys.modules, {"wx": mock_wx})
mocker.patch("basilisk.presenters.preferences_presenter.set_log_level")
presenter, mock_conf = make_presenter(view=mock_view)
mock_conf.save.side_effect = OSError("disk full")

presenter.on_ok()

mock_view.show_error.assert_called_once()
mock_view.EndModal.assert_not_called()

def test_validation_error_on_save_shows_error_and_keeps_dialog_open(
self, mock_view, make_presenter, mocker
):
"""ValidationError from conf.save() shows error and does not close dialog."""
from pydantic import BaseModel, ValidationError

mock_wx = MagicMock()
mocker.patch.dict(sys.modules, {"wx": mock_wx})
mocker.patch("basilisk.presenters.preferences_presenter.set_log_level")
presenter, mock_conf = make_presenter(view=mock_view)

class _M(BaseModel):
x: int

try:
_M(x="bad")
except ValidationError as exc:
mock_conf.save.side_effect = exc

presenter.on_ok()

mock_view.show_error.assert_called_once()
mock_view.EndModal.assert_not_called()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Make these error-path tests configure the new template-path widgets explicitly.

on_ok() now reads html_message_template_path.get_path() and html_export_template_path.get_path() before save(), but these tests currently succeed via MagicMock autovivification. That means interface regressions around the new widgets will not fail the suite. Set both attributes on mock_view to explicit mocks with get_path.return_value so this path is exercised realistically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/presenters/test_preferences_presenter.py` around lines 141 - 178, The
tests test_oserror_on_save_shows_error_and_keeps_dialog_open and
test_validation_error_on_save_shows_error_and_keeps_dialog_open should
explicitly set mock_view.html_message_template_path and
mock_view.html_export_template_path to mocks that have get_path.return_value
(e.g., MagicMock(get_path=MagicMock(return_value="..."))) so presenter.on_ok()
calls to html_message_template_path.get_path() and
html_export_template_path.get_path() are exercised before mock_conf.save() is
invoked; update both tests to assign those attributes on mock_view prior to
calling presenter.on_ok() so autovivification does not hide interface
regressions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant