From e2411f3e7551e081f0ee2260d4ae3a2545496b15 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 13:59:32 +0200 Subject: [PATCH 01/14] fix: remove browser_state from settings change callbacks The .change callbacks no longer write to browser_state, which could re-trigger .load and cascade into an infinite loop. Settings are now persisted to browser_state only on submit/retry, and session_state is updated immediately on change. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 84 +++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 3668440f4..7b67f373f 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -384,7 +384,8 @@ def initialize_browser_settings(browser_state, session_state: SessionState): profile_markdown = _profile_models_markdown(model_profile) return openrouter_api_key, model, speedvsdetail, model_profile, profile_markdown, "", browser_state, session_state -def update_browser_settings_callback(openrouter_api_key, model, speedvsdetail, model_profile, browser_state, session_state: SessionState): +def save_browser_settings_callback(openrouter_api_key, model, speedvsdetail, model_profile, browser_state): + """Persist current settings to BrowserState. Called on submit/retry, not on every change.""" try: settings = json.loads(browser_state) if browser_state else {} except Exception: @@ -393,13 +394,7 @@ def update_browser_settings_callback(openrouter_api_key, model, speedvsdetail, m settings["model_radio"] = model settings["speedvsdetail_radio"] = speedvsdetail settings["model_profile_radio"] = model_profile - updated_browser_state = json.dumps(settings) - session_state.openrouter_api_key = openrouter_api_key - session_state.llm_model = model - session_state.speedvsdetail = speedvsdetail - session_state.model_profile = model_profile - profile_markdown = _profile_models_markdown(model_profile) - return updated_browser_state, openrouter_api_key, model, speedvsdetail, model_profile, profile_markdown, "", session_state + return json.dumps(settings) def run_planner(submit_or_retry_button, plan_prompt, browser_state, session_state: SessionState): """ @@ -848,6 +843,10 @@ def check_api_key(session_state: SessionState): fn=clear_status, inputs=session_state, outputs=[status_markdown, session_state] + ).then( + fn=save_browser_settings_callback, + inputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, browser_state], + outputs=[browser_state] ).then( fn=run_planner, inputs=[submit_btn, prompt_input, browser_state, session_state], @@ -861,6 +860,10 @@ def check_api_key(session_state: SessionState): fn=clear_status, inputs=session_state, outputs=[status_markdown, session_state] + ).then( + fn=save_browser_settings_callback, + inputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, browser_state], + outputs=[browser_state] ).then( fn=run_planner, inputs=[retry_btn, prompt_input, browser_state, session_state], @@ -889,45 +892,46 @@ def check_api_key(session_state: SessionState): # The download file value is updated by run_planner generator outputs. # Unified change callbacks for settings. + # NOTE: trigger="change" is Gradio's default. We must NOT output back to + # any component that is also an input — that would create an infinite + # client-side event loop (component changes → callback → component changes → …). + # We also avoid outputting to browser_state here; BrowserState updates + # can re-trigger .load in some Gradio versions, causing a cascade. + # Instead, browser_state is only written by initialize_browser_settings on load. + settings_change_inputs = [openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, session_state] + settings_change_outputs = [profile_models_markdown, active_config_markdown, session_state] + + def update_settings_on_change(openrouter_api_key, model, speedvsdetail, model_profile, session_state: SessionState): + session_state.openrouter_api_key = openrouter_api_key + session_state.llm_model = model + session_state.speedvsdetail = speedvsdetail + session_state.model_profile = model_profile + profile_markdown = _profile_models_markdown(model_profile) + return profile_markdown, "", session_state + openrouter_api_key_text.change( - fn=update_browser_settings_callback, - inputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, browser_state, session_state], - outputs=[browser_state, openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, session_state] - ).then( - fn=check_api_key, - inputs=[session_state], - outputs=[api_key_warning] - ) + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) model_radio.change( - fn=update_browser_settings_callback, - inputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, browser_state, session_state], - outputs=[browser_state, openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, session_state] - ).then( - fn=check_api_key, - inputs=[session_state], - outputs=[api_key_warning] - ) + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) speedvsdetail_radio.change( - fn=update_browser_settings_callback, - inputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, browser_state, session_state], - outputs=[browser_state, openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, session_state] - ).then( - fn=check_api_key, - inputs=[session_state], - outputs=[api_key_warning] - ) + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) model_profile_radio.change( - fn=update_browser_settings_callback, - inputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, browser_state, session_state], - outputs=[browser_state, openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, session_state] - ).then( - fn=check_api_key, - inputs=[session_state], - outputs=[api_key_warning] - ) + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) purge_button.click( fn=trigger_purge_runs, From 72964528dd17e105b4455cd868fbf52889d005e4 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:08:58 +0200 Subject: [PATCH 02/14] debug: strip Settings tab to bare minimum to isolate hang All components hidden except a placeholder markdown. If the tab still hangs, the issue is outside the Settings tab. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 57 +++++++------------------------------ 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 7b67f373f..ec2dd97d7 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -751,68 +751,33 @@ def check_api_key(session_state: SessionState): ) with gr.Tab("Settings"): - speedvsdetail_items = [ - ("Ping", SpeedVsDetailEnum.PING_LLM), - ("All details, but slow", SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW), - ("Fast, but few details", SpeedVsDetailEnum.FAST_BUT_SKIP_DETAILS), - ] + gr.Markdown("Settings tab placeholder — testing if tab opens without hanging.") speedvsdetail_radio = gr.Radio( - speedvsdetail_items, + [("All details, but slow", SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW)], value=SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW, label="Speed vs Detail", - interactive=True + interactive=True, + visible=False, ) - - if CONFIG.visible_llm_info: - if llm_info.ollama_status == OllamaStatus.ollama_not_running: - gr.Markdown("**Ollama is not running**, so Ollama models are unavailable. Please start Ollama to use them.") - elif llm_info.ollama_status == OllamaStatus.mixed: - gr.Markdown("**Mixed. Some Ollama models are running, but some are NOT running.**, You may have to start the ones that aren't running.") - - if len(llm_info.error_message_list) > 0: - gr.Markdown("**Error messages:**") - for error_message in llm_info.error_message_list: - gr.Markdown(f"- {error_message}") - model_radio = gr.Radio( - available_model_names, + available_model_names[:1] if available_model_names else [("default", "default")], value=default_model_value, label="Model", - interactive=True + interactive=True, + visible=False, ) - model_profile_radio = gr.Radio( - [ - ("Baseline", ModelProfileEnum.BASELINE.value), - ("Premium", ModelProfileEnum.PREMIUM.value), - ("Frontier", ModelProfileEnum.FRONTIER.value), - ("Custom", ModelProfileEnum.CUSTOM.value), - ], + [("Baseline", ModelProfileEnum.BASELINE.value)], value=ModelProfileEnum.BASELINE.value, label="Model Profile", - info="Select which profile file is used by auto model selection.", interactive=True, + visible=False, ) - gr.Markdown( - "\n".join( - [ - "**Profile details**", - "- `baseline` -> `llm_config/baseline.json` (default balanced profile).", - "- `premium` -> `llm_config/premium.json` (higher-cost model ordering).", - "- `frontier` -> `llm_config/frontier.json` (most capable model ordering).", - "- `custom` -> `llm_config/custom.json` or `PLANEXE_LLM_CONFIG_CUSTOM_FILENAME` (filename only, e.g. `custom.json`).", - "- The exact models come from the selected JSON file priorities.", - ] - ) - ) - profile_models_markdown = gr.Markdown(_profile_models_markdown(ModelProfileEnum.BASELINE.value)) - + profile_models_markdown = gr.Markdown("", visible=False) openrouter_api_key_text = gr.Textbox( label="OpenRouter API Key", type="password", - placeholder="Enter your OpenRouter API key (required)", - info="Sign up at [OpenRouter](https://openrouter.ai/) to get an API key. A small top-up (e.g. 5 USD) is needed to access paid models.", - visible=CONFIG.visible_openrouter_api_key_textbox + visible=False, ) with gr.Tab("Advanced"): From cc508db6c4023918e45054767057b9e36adafb12 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:14:21 +0200 Subject: [PATCH 03/14] debug: disable gr.Examples to test if it causes the tab hang Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index ec2dd97d7..3c19c2acf 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -745,10 +745,11 @@ def check_api_key(session_state: SessionState): download_output = gr.File(label="Download latest output (excluding log.txt) as zip") with gr.Column(scale=1, min_width=300): - examples = gr.Examples( - examples=gradio_examples, - inputs=[prompt_input], - ) + gr.Markdown("*(Examples disabled for debugging)*") + # examples = gr.Examples( + # examples=gradio_examples, + # inputs=[prompt_input], + # ) with gr.Tab("Settings"): gr.Markdown("Settings tab placeholder — testing if tab opens without hanging.") From a013d04fd25f654084cef8462ad2b3538a0db74a Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:16:31 +0200 Subject: [PATCH 04/14] debug: disable ALL settings callbacks and .load handlers If tabs still hang, the issue is in the UI components or Gradio itself. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 69 ++++--------------------------------- 1 file changed, 7 insertions(+), 62 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 3c19c2acf..429bb40b0 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,68 +857,13 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # Unified change callbacks for settings. - # NOTE: trigger="change" is Gradio's default. We must NOT output back to - # any component that is also an input — that would create an infinite - # client-side event loop (component changes → callback → component changes → …). - # We also avoid outputting to browser_state here; BrowserState updates - # can re-trigger .load in some Gradio versions, causing a cascade. - # Instead, browser_state is only written by initialize_browser_settings on load. - settings_change_inputs = [openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, session_state] - settings_change_outputs = [profile_models_markdown, active_config_markdown, session_state] - - def update_settings_on_change(openrouter_api_key, model, speedvsdetail, model_profile, session_state: SessionState): - session_state.openrouter_api_key = openrouter_api_key - session_state.llm_model = model - session_state.speedvsdetail = speedvsdetail - session_state.model_profile = model_profile - profile_markdown = _profile_models_markdown(model_profile) - return profile_markdown, "", session_state - - openrouter_api_key_text.change( - fn=update_settings_on_change, - inputs=settings_change_inputs, - outputs=settings_change_outputs, - ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) - - model_radio.change( - fn=update_settings_on_change, - inputs=settings_change_inputs, - outputs=settings_change_outputs, - ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) - - speedvsdetail_radio.change( - fn=update_settings_on_change, - inputs=settings_change_inputs, - outputs=settings_change_outputs, - ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) - - model_profile_radio.change( - fn=update_settings_on_change, - inputs=settings_change_inputs, - outputs=settings_change_outputs, - ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) - - purge_button.click( - fn=trigger_purge_runs, - inputs=[purge_max_age_hours, session_state], - outputs=[purge_status, session_state] - ) - - # Initialize settings on load from persistent browser_state. - demo_text2plan.load( - fn=initialize_browser_settings, - inputs=[browser_state, session_state], - outputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, browser_state, session_state] - ).then( - fn=check_api_key, - inputs=[session_state], - outputs=[api_key_warning] - ) - demo_text2plan.load( - fn=update_open_dir_button_visibility, - outputs=[open_dir_btn] - ) + # DEBUG: All settings callbacks, .load handlers disabled to isolate hang. + # purge_button.click(...) + # openrouter_api_key_text.change(...) + # model_radio.change(...) + # speedvsdetail_radio.change(...) + # model_profile_radio.change(...) + # demo_text2plan.load(...) def run_app(): # print("Environment variables Gradio:\n" + get_env_as_string() + "\n\n\n") From c1720a42f3a69c53bc0eb22af242bf4ed384c785 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:18:57 +0200 Subject: [PATCH 05/14] debug: re-enable only .load handlers Testing if the hang is caused by .load or by .change callbacks. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 429bb40b0..488531300 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,13 +857,20 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: All settings callbacks, .load handlers disabled to isolate hang. - # purge_button.click(...) - # openrouter_api_key_text.change(...) - # model_radio.change(...) - # speedvsdetail_radio.change(...) - # model_profile_radio.change(...) - # demo_text2plan.load(...) + # DEBUG: Only .load handlers re-enabled. + demo_text2plan.load( + fn=initialize_browser_settings, + inputs=[browser_state, session_state], + outputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, browser_state, session_state] + ).then( + fn=check_api_key, + inputs=[session_state], + outputs=[api_key_warning] + ) + demo_text2plan.load( + fn=update_open_dir_button_visibility, + outputs=[open_dir_btn] + ) def run_app(): # print("Environment variables Gradio:\n" + get_env_as_string() + "\n\n\n") From 3778e6f83bf28be9641d485387ff659123756885 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:20:54 +0200 Subject: [PATCH 06/14] debug: .load only updates session_state, not settings components Testing if outputting to radio/textbox components from .load causes hang. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 488531300..b6dc8ff03 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,11 +857,22 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: Only .load handlers re-enabled. + # DEBUG: .load with minimal outputs (no settings components). + def initialize_browser_settings_minimal(browser_state, session_state: SessionState): + try: + settings = json.loads(browser_state) if browser_state else {} + except Exception: + settings = {} + session_state.openrouter_api_key = settings.get("openrouter_api_key_text", "") + session_state.llm_model = settings.get("model_radio", default_model_value) + session_state.speedvsdetail = settings.get("speedvsdetail_radio", SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW) + session_state.model_profile = settings.get("model_profile_radio", ModelProfileEnum.BASELINE.value) + return session_state + demo_text2plan.load( - fn=initialize_browser_settings, + fn=initialize_browser_settings_minimal, inputs=[browser_state, session_state], - outputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, browser_state, session_state] + outputs=[session_state] ).then( fn=check_api_key, inputs=[session_state], From 089b7fcb46b88043db1c1dd854e90ff019078d97 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:25:20 +0200 Subject: [PATCH 07/14] debug: disable browser_state .load entirely, keep only open_dir .load Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index b6dc8ff03..eacd3da04 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,27 +857,7 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: .load with minimal outputs (no settings components). - def initialize_browser_settings_minimal(browser_state, session_state: SessionState): - try: - settings = json.loads(browser_state) if browser_state else {} - except Exception: - settings = {} - session_state.openrouter_api_key = settings.get("openrouter_api_key_text", "") - session_state.llm_model = settings.get("model_radio", default_model_value) - session_state.speedvsdetail = settings.get("speedvsdetail_radio", SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW) - session_state.model_profile = settings.get("model_profile_radio", ModelProfileEnum.BASELINE.value) - return session_state - - demo_text2plan.load( - fn=initialize_browser_settings_minimal, - inputs=[browser_state, session_state], - outputs=[session_state] - ).then( - fn=check_api_key, - inputs=[session_state], - outputs=[api_key_warning] - ) + # DEBUG: Only open_dir .load — no browser_state involved. demo_text2plan.load( fn=update_open_dir_button_visibility, outputs=[open_dir_btn] From 3398134fcefa3723af4ac865155304a77acfccb8 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:27:48 +0200 Subject: [PATCH 08/14] debug: disable ALL .load handlers including open_dir Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index eacd3da04..0d10c0aa9 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,11 +857,8 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: Only open_dir .load — no browser_state involved. - demo_text2plan.load( - fn=update_open_dir_button_visibility, - outputs=[open_dir_btn] - ) + # DEBUG: All .load handlers disabled. + pass def run_app(): # print("Environment variables Gradio:\n" + get_env_as_string() + "\n\n\n") From be1099ae46dfbdea766c56c66f239f388c759a34 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:30:49 +0200 Subject: [PATCH 09/14] debug: re-enable only update_open_dir .load handler Testing if a single .load is fine on its own. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 0d10c0aa9..d8accab06 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,8 +857,11 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: All .load handlers disabled. - pass + # DEBUG: Only open_dir .load — testing if a single .load works. + demo_text2plan.load( + fn=update_open_dir_button_visibility, + outputs=[open_dir_btn] + ) def run_app(): # print("Environment variables Gradio:\n" + get_env_as_string() + "\n\n\n") From f07048e92bdd85d683241bf805cfc8b2e4542348 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:34:42 +0200 Subject: [PATCH 10/14] debug: .load with no-op lambda, no outputs Testing if .load itself causes the hang regardless of what it does. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index d8accab06..0629bb2db 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,10 +857,9 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: Only open_dir .load — testing if a single .load works. + # DEBUG: .load with a no-op function, no outputs. demo_text2plan.load( - fn=update_open_dir_button_visibility, - outputs=[open_dir_btn] + fn=lambda: None, ) def run_app(): From 1d7f7bdeeeec262227dd8e077ddbd1c53a8ea485 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:36:49 +0200 Subject: [PATCH 11/14] debug: .load outputs to status_markdown Testing if any .load output causes hang, or only specific components. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 0629bb2db..52100e9cc 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,9 +857,10 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: .load with a no-op function, no outputs. + # DEBUG: .load outputting to a markdown component. demo_text2plan.load( - fn=lambda: None, + fn=lambda: "loaded", + outputs=[status_markdown] ) def run_app(): From 31614e0274ceab29f1a978ace60358511871c0a5 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:39:37 +0200 Subject: [PATCH 12/14] debug: use browser_state.change instead of .load for settings init Gradio 6.11 .load with outputs breaks tab switching. BrowserState fires .change when it loads from localStorage, so we can use that to restore settings instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 52100e9cc..b65d9d6e9 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -857,10 +857,17 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. - # DEBUG: .load outputting to a markdown component. - demo_text2plan.load( - fn=lambda: "loaded", - outputs=[status_markdown] + # Restore settings from BrowserState when it loads from localStorage. + # We use browser_state.change instead of demo_text2plan.load because + # Gradio 6.11 has a bug where .load with outputs breaks tab switching. + browser_state.change( + fn=initialize_browser_settings, + inputs=[browser_state, session_state], + outputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, browser_state, session_state] + ).then( + fn=check_api_key, + inputs=[session_state], + outputs=[api_key_warning] ) def run_app(): From e903a6b59467725a1e27fe289d808217a4f998a2 Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:42:13 +0200 Subject: [PATCH 13/14] fix: restore all Settings components and callbacks, no .load Root cause: Gradio 6.11 .load with outputs breaks tab switching. Fix: use browser_state.change for settings initialization instead. All Settings tab components, Examples, .change callbacks, and purge button restored. Zero .load handlers used. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 108 ++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index b65d9d6e9..2f338643f 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -745,40 +745,74 @@ def check_api_key(session_state: SessionState): download_output = gr.File(label="Download latest output (excluding log.txt) as zip") with gr.Column(scale=1, min_width=300): - gr.Markdown("*(Examples disabled for debugging)*") - # examples = gr.Examples( - # examples=gradio_examples, - # inputs=[prompt_input], - # ) + examples = gr.Examples( + examples=gradio_examples, + inputs=[prompt_input], + ) with gr.Tab("Settings"): - gr.Markdown("Settings tab placeholder — testing if tab opens without hanging.") + speedvsdetail_items = [ + ("Ping", SpeedVsDetailEnum.PING_LLM), + ("All details, but slow", SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW), + ("Fast, but few details", SpeedVsDetailEnum.FAST_BUT_SKIP_DETAILS), + ] speedvsdetail_radio = gr.Radio( - [("All details, but slow", SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW)], + speedvsdetail_items, value=SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW, label="Speed vs Detail", - interactive=True, - visible=False, + interactive=True ) + + if CONFIG.visible_llm_info: + if llm_info.ollama_status == OllamaStatus.ollama_not_running: + gr.Markdown("**Ollama is not running**, so Ollama models are unavailable. Please start Ollama to use them.") + elif llm_info.ollama_status == OllamaStatus.mixed: + gr.Markdown("**Mixed. Some Ollama models are running, but some are NOT running.**, You may have to start the ones that aren't running.") + + if len(llm_info.error_message_list) > 0: + gr.Markdown("**Error messages:**") + for error_message in llm_info.error_message_list: + gr.Markdown(f"- {error_message}") + model_radio = gr.Radio( - available_model_names[:1] if available_model_names else [("default", "default")], + available_model_names, value=default_model_value, label="Model", - interactive=True, - visible=False, + interactive=True ) + model_profile_radio = gr.Radio( - [("Baseline", ModelProfileEnum.BASELINE.value)], + [ + ("Baseline", ModelProfileEnum.BASELINE.value), + ("Premium", ModelProfileEnum.PREMIUM.value), + ("Frontier", ModelProfileEnum.FRONTIER.value), + ("Custom", ModelProfileEnum.CUSTOM.value), + ], value=ModelProfileEnum.BASELINE.value, label="Model Profile", + info="Select which profile file is used by auto model selection.", interactive=True, - visible=False, ) - profile_models_markdown = gr.Markdown("", visible=False) + gr.Markdown( + "\n".join( + [ + "**Profile details**", + "- `baseline` -> `llm_config/baseline.json` (default balanced profile).", + "- `premium` -> `llm_config/premium.json` (higher-cost model ordering).", + "- `frontier` -> `llm_config/frontier.json` (most capable model ordering).", + "- `custom` -> `llm_config/custom.json` or `PLANEXE_LLM_CONFIG_CUSTOM_FILENAME` (filename only, e.g. `custom.json`).", + "- The exact models come from the selected JSON file priorities.", + ] + ) + ) + profile_models_markdown = gr.Markdown(_profile_models_markdown(ModelProfileEnum.BASELINE.value)) + openrouter_api_key_text = gr.Textbox( label="OpenRouter API Key", type="password", - visible=False, + placeholder="Enter your OpenRouter API key (required)", + info="Sign up at [OpenRouter](https://openrouter.ai/) to get an API key. A small top-up (e.g. 5 USD) is needed to access paid models.", + visible=CONFIG.visible_openrouter_api_key_textbox ) with gr.Tab("Advanced"): @@ -857,6 +891,48 @@ def check_api_key(session_state: SessionState): ) # The download file value is updated by run_planner generator outputs. + # Settings change callbacks — update session_state and profile display. + settings_change_inputs = [openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, session_state] + settings_change_outputs = [profile_models_markdown, active_config_markdown, session_state] + + def update_settings_on_change(openrouter_api_key, model, speedvsdetail, model_profile, session_state: SessionState): + session_state.openrouter_api_key = openrouter_api_key + session_state.llm_model = model + session_state.speedvsdetail = speedvsdetail + session_state.model_profile = model_profile + profile_markdown = _profile_models_markdown(model_profile) + return profile_markdown, "", session_state + + openrouter_api_key_text.change( + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) + + model_radio.change( + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) + + speedvsdetail_radio.change( + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) + + model_profile_radio.change( + fn=update_settings_on_change, + inputs=settings_change_inputs, + outputs=settings_change_outputs, + ).then(fn=check_api_key, inputs=[session_state], outputs=[api_key_warning]) + + purge_button.click( + fn=trigger_purge_runs, + inputs=[purge_max_age_hours, session_state], + outputs=[purge_status, session_state] + ) + # Restore settings from BrowserState when it loads from localStorage. # We use browser_state.change instead of demo_text2plan.load because # Gradio 6.11 has a bug where .load with outputs breaks tab switching. From b487278f9f28fb633fc4483b5e4eb4d0512bac0f Mon Sep 17 00:00:00 2001 From: Simon Strandgaard Date: Fri, 10 Apr 2026 14:46:28 +0200 Subject: [PATCH 14/14] fix: hide Open Output Dir button when service is not running Moved the runtime visibility check into initialize_browser_settings since .load handlers can no longer be used. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend_single_user/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend_single_user/app.py b/frontend_single_user/app.py index 2f338643f..ff789b943 100644 --- a/frontend_single_user/app.py +++ b/frontend_single_user/app.py @@ -382,7 +382,8 @@ def initialize_browser_settings(browser_state, session_state: SessionState): session_state.speedvsdetail = speedvsdetail session_state.model_profile = model_profile profile_markdown = _profile_models_markdown(model_profile) - return openrouter_api_key, model, speedvsdetail, model_profile, profile_markdown, "", browser_state, session_state + open_dir_visible = gr.update(visible=is_open_dir_service_running()) + return openrouter_api_key, model, speedvsdetail, model_profile, profile_markdown, "", open_dir_visible, browser_state, session_state def save_browser_settings_callback(openrouter_api_key, model, speedvsdetail, model_profile, browser_state): """Persist current settings to BrowserState. Called on submit/retry, not on every change.""" @@ -939,7 +940,7 @@ def update_settings_on_change(openrouter_api_key, model, speedvsdetail, model_pr browser_state.change( fn=initialize_browser_settings, inputs=[browser_state, session_state], - outputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, browser_state, session_state] + outputs=[openrouter_api_key_text, model_radio, speedvsdetail_radio, model_profile_radio, profile_models_markdown, active_config_markdown, open_dir_btn, browser_state, session_state] ).then( fn=check_api_key, inputs=[session_state],