From 036aba53af9d56894b8b3c070e9a6161c1204032 Mon Sep 17 00:00:00 2001 From: Pat Lacey Date: Wed, 9 Sep 2026 17:02:12 -0400 Subject: [PATCH 1/3] fix: restore enharmonic keys from generation history --- src/conductor_main/app.py | 52 ++++++++++++++++++++++----------- tests/test_app.py | 60 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/conductor_main/app.py b/src/conductor_main/app.py index 7f1f53d..bd5556c 100644 --- a/src/conductor_main/app.py +++ b/src/conductor_main/app.py @@ -25,7 +25,7 @@ LoopGenerationEngine, ProviderCredentials, ) -from conductor_core.music import get_loop_prompt, get_model_info +from conductor_core.music import ENHARMONIC_NOTE_NAMES, get_loop_prompt, get_model_info from conductor_core.playback import ( add_soundfont_search_dir, get_default_soundfont, @@ -44,6 +44,20 @@ DEFAULT_MODEL = "gemini-3.1-flash-lite" CONDUCTOR_APP_DIRNAME = "main" MAX_HISTORY_GENERATIONS = 20 +KEY_CHOICES = ( + "C", + "C#/Db", + "D", + "D#/Eb", + "E", + "F", + "F#/Gb", + "G", + "G#/Ab", + "A", + "A#/Bb", + "B", +) SHARP_KEY_ALIASES = { "C#/Db": "C#", "D#/Eb": "D#", @@ -51,6 +65,11 @@ "G#/Ab": "G#", "A#/Bb": "A#", } +CORE_KEY_TO_UI = { + key: KEY_CHOICES[pitch_class] + for pitch_class, keys in enumerate(ENHARMONIC_NOTE_NAMES) + for key in keys +} APP_CSS = """ .center-title { text-align: center; font-size: 3em; } .app-header { @@ -123,6 +142,14 @@ def normalize_key_for_core(key): return SHARP_KEY_ALIASES.get(key, key) +def normalize_key_for_ui(key): + """Coerce a supported Core key spelling to its 12-note UI choice.""" + if not isinstance(key, str): + return None + + return CORE_KEY_TO_UI.get(normalize_key_for_core(key)) + + def get_generation(gen_id): return HISTORY_STORE.get_generation(gen_id) @@ -223,6 +250,12 @@ def get_history_control_updates(gen): provider = gen.provider model = gen.model warnings = [] + ui_key = normalize_key_for_ui(gen.key) + if ui_key is None: + key_update = gr.update() + warnings.append(f"Unavailable key: {gen.key!r}.") + else: + key_update = gr.update(value=ui_key) provider_choices = list(known_models) provider_is_available = provider in known_models @@ -273,7 +306,7 @@ def get_history_control_updates(gen): effort_options.append(effort_value) return HistoryControlUpdates( - key=gr.update(value=gen.key), + key=key_update, scale=gr.update(value=gen.scale), description=gr.update(value=gen.prompt), provider=gr.update(choices=provider_choices, value=provider), @@ -1109,20 +1142,7 @@ def create_demo(playback_status=None): with gr.Column(): gr.Markdown("## Loop Parameters") key_input = gr.Dropdown( - choices=[ - "C", - "C#/Db", - "D", - "D#/Eb", - "E", - "F", - "F#/Gb", - "G", - "G#/Ab", - "A", - "A#/Bb", - "B", - ], + choices=KEY_CHOICES, label="Key", value="C", ) diff --git a/tests/test_app.py b/tests/test_app.py index b9d487a..8b7d76e 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -27,6 +27,26 @@ def test_normalize_key_for_core_defaults_combined_black_keys_to_sharps(): } == expected_keys +def test_normalize_key_for_ui_coerces_core_enharmonics_to_twelve_choices(): + expected_keys = { + "C#": "C#/Db", + "Db": "C#/Db", + "D#": "D#/Eb", + "F#": "F#/Gb", + "G#": "G#/Ab", + "A#": "A#/Bb", + "C##": "D", + "Gbb": "F", + "B#": "C", + "C": "C", + } + + assert { + key: app.normalize_key_for_ui(key) for key in expected_keys + } == expected_keys + assert app.normalize_key_for_ui("not-a-key") is None + + def test_run_loop_passes_ui_configuration_to_core(monkeypatch, tmp_path): captured = {} midi_path = tmp_path / "loop.mid" @@ -277,6 +297,42 @@ def test_history_controls_restore_known_effort_model_exactly(monkeypatch): assert updates.warnings == () +def test_history_controls_warn_and_preserve_key_for_invalid_saved_value(monkeypatch): + monkeypatch.setattr( + app, + "get_model_info", + lambda: { + "models": { + "OpenAI": { + "known-model": { + "extended_thinking": False, + "effort_options": [], + } + } + } + }, + ) + monkeypatch.setattr(app.gr, "update", lambda **kwargs: kwargs) + + updates = app.get_history_control_updates( + SimpleNamespace( + key="not-a-key", + scale="Major", + prompt="restored prompt", + provider="OpenAI", + model="known-model", + temperature=0.5, + use_thinking=False, + effort="low", + ) + ) + + assert updates.key == {} + assert updates.scale == {"value": "Major"} + assert updates.description == {"value": "restored prompt"} + assert updates.warnings == ("Unavailable key: 'not-a-key'.",) + + def test_history_controls_restore_known_toggle_model_exactly(monkeypatch): monkeypatch.setattr( app, @@ -510,7 +566,7 @@ def test_load_history_item_warns_when_saved_soundfont_is_missing(monkeypatch, tm audio_path=str(audio_path), soundfont="missing.sf2", id=gen_id, - key="D", + key="C#", scale="minor", prompt="saved prompt", provider="Google", @@ -556,7 +612,7 @@ def test_load_history_item_warns_when_saved_soundfont_is_missing(monkeypatch, tm assert saved_soundfont == "missing.sf2" assert current_audio_path == str(audio_path) assert rerender_update["interactive"] is True - assert key_update["value"] == "D" + assert key_update["value"] == "C#/Db" assert scale_update["value"] == "minor" assert description_update["value"] == "saved prompt" assert provider_update["value"] == "Google" From 72ae2b6e9308f10845cccafb9df672d5312aacb2 Mon Sep 17 00:00:00 2001 From: Pat Lacey Date: Wed, 9 Sep 2026 17:23:14 -0400 Subject: [PATCH 2/3] fix: follow theme colors in generation history --- src/conductor_main/app.py | 58 +++++++++++++++++++++++++++++---------- tests/test_app.py | 12 ++++++++ 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/conductor_main/app.py b/src/conductor_main/app.py index bd5556c..254528a 100644 --- a/src/conductor_main/app.py +++ b/src/conductor_main/app.py @@ -83,15 +83,49 @@ z-index: 1; } .history-sidebar { - background: #1a1a1a; - border-left: 1px solid #333; + background: var(--background-fill-primary); + border-left: 1px solid var(--border-color-primary); + color: var(--body-text-color); height: 100%; overflow-y: auto; } +.history-item { + background: var(--block-background-fill); + border: 1px solid var(--border-color-primary); + border-radius: 8px; + padding: 12px; + margin-bottom: 10px; +} .history-item:hover { - border-color: #666 !important; + border-color: var(--border-color-accent) !important; cursor: pointer; } +.history-item-title { + color: var(--body-text-color); + font-weight: bold; + margin-bottom: 4px; +} +.history-item-prompt { + color: var(--block-label-text-color); + font-size: 0.85em; + margin-bottom: 6px; +} +.history-item-metadata { + color: var(--block-label-text-color); + display: flex; + font-size: 0.8em; + justify-content: space-between; +} +.history-item-cost { + color: var(--block-label-text-color); + font-size: 0.75em; + margin-top: 4px; +} +.history-empty { + color: var(--block-label-text-color); + padding: 20px; + text-align: center; +} """ @@ -851,7 +885,7 @@ def render_history_html(): if not history: return """ -
+

No generations yet.

Your generated loops will appear here.

@@ -872,24 +906,18 @@ def render_history_html(): reasoning_suffix = f" ({reasoning})" if reasoning else "" html_parts.append(f""" -
-
+
+
{key} {scale}
-
+
"{prompt_preview}"
-
+ -
+
Cost: {cost_str}
diff --git a/tests/test_app.py b/tests/test_app.py index 8b7d76e..6bbd855 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -796,6 +796,18 @@ def test_render_history_html_displays_zero_cost(monkeypatch): assert "Cost: N/A" not in html +def test_history_uses_theme_aware_styles(monkeypatch): + monkeypatch.setattr(app, "load_history", list) + + rendered_history = app.render_history_html() + + assert 'class="history-empty"' in rendered_history + assert "background: var(--background-fill-primary)" in app.APP_CSS + assert "background: var(--block-background-fill)" in app.APP_CSS + assert "color: var(--body-text-color)" in app.APP_CSS + assert "color: var(--block-label-text-color)" in app.APP_CSS + + def test_render_history_html_displays_missing_cost_as_na(monkeypatch): monkeypatch.setattr( app, From 73a15bee030805e36bcd7f137a0239eef12b7c7e Mon Sep 17 00:00:00 2001 From: Pat Lacey Date: Wed, 9 Sep 2026 17:59:23 -0400 Subject: [PATCH 3/3] test: drop CSS string assertions from theme style test --- tests/test_app.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 6bbd855..6a5289a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -796,16 +796,12 @@ def test_render_history_html_displays_zero_cost(monkeypatch): assert "Cost: N/A" not in html -def test_history_uses_theme_aware_styles(monkeypatch): +def test_render_history_html_uses_theme_aware_classes(monkeypatch): monkeypatch.setattr(app, "load_history", list) rendered_history = app.render_history_html() assert 'class="history-empty"' in rendered_history - assert "background: var(--background-fill-primary)" in app.APP_CSS - assert "background: var(--block-background-fill)" in app.APP_CSS - assert "color: var(--body-text-color)" in app.APP_CSS - assert "color: var(--block-label-text-color)" in app.APP_CSS def test_render_history_html_displays_missing_cost_as_na(monkeypatch):