Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude/skills/create-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,31 @@ The 4 pages call these methods respectively:
- `self.ui.input_TOPP("ToolName", custom_defaults={})` — auto-generated TOPP parameter UI
- `self.ui.input_python("script_name")` — auto-generated Python tool parameter UI
- `self.ui.input_widget(key, default, label)` — single custom widget
- `select_input_file`, `input_TOPP` and `input_widget` accept `reactive=True` to rerun `configure()` when the widget changes (for conditional UI — see below)

### Logging
- `self.logger.log("message")` — log progress during execution

## Conditional UI (reactive)

Parameter widgets are isolated in an `st.fragment` by default, so a change reruns only that
widget and can't show/hide other widgets. Pass `reactive=True` to render the widget in the
parent scope instead — a change then reruns `configure()`. Read the live value from
`st.session_state` (not `self.params`, which is stale within the rerun) using
`self.parameter_manager.param_prefix` for custom-widget keys or `topp_param_prefix` for TOPP
keys of the form `"<Tool>:1:<param path>"`.

```python
@st.fragment
def configure(self) -> None:
pm = self.parameter_manager
# changing the tool's `type` selectbox reruns configure()
self.ui.input_TOPP("IsobaricAnalyzer", reactive=True)
iso_type = st.session_state.get(f"{pm.topp_param_prefix}IsobaricAnalyzer:1:type", "")
if iso_type.startswith("tmt"):
self.ui.input_widget("tmt-channels", 10, "TMT channels", widget_type="number")
```

## Reference Files

- Example workflow: `src/Workflow.py`
Expand All @@ -144,6 +165,7 @@ The 4 pages call these methods respectively:
- [ ] `__init__` calls `super().__init__("Name", st.session_state["workspace"])`
- [ ] `upload()`, `configure()`, `execution()`, `results()` implemented
- [ ] `@st.fragment` on `configure()` and `results()`
- [ ] `reactive=True` on any widget whose value controls other widgets' visibility
- [ ] 4 content pages created in `content/`
- [ ] All 4 pages registered as a group in `app.py`
- [ ] Default parameters added to `default-parameters.json` if needed
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ Each workflow gets 4 content pages (upload, configure, run, results) that call `

Decorate `configure()` and `results()` with `@st.fragment` for partial reruns.

For conditional UI (a widget that shows/hides other widgets), pass `reactive=True` to `input_widget`, `select_input_file`, or `input_TOPP` so a change reruns the parent `configure()` instead of only its isolated fragment. Read the changed value from `st.session_state` (not `self.params`, which is stale within the rerun) via `parameter_manager.param_prefix` for custom-widget keys or `topp_param_prefix` for TOPP keys (`"<Tool>:1:<param path>"`).

### Python Tools

Custom scripts in `src/python-tools/` define a `DEFAULTS` list for auto-generated UI:
Expand Down
2 changes: 2 additions & 0 deletions docs/build_app.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,5 @@ Take a look at the example pages `Simple Workflow` or `Workflow with mzML files`
### Complex workflow using TOPP tools

This template app features a module in `src/workflow` that allows for complex and long workflows to be built very efficiently. Check out the `TOPP Workflow Framework` page for more information (on the *sidebar*).

For building **conditional parameter UI** (widgets that appear or disappear based on another parameter's value), see the *Reactive parameters* subsection of the `TOPP Workflow Framework` page's *Parameter Input* section.
32 changes: 32 additions & 0 deletions docs/toppframework.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,38 @@ def content():
"💡 Access parameter widget values by their **key** in the `self.params` object, e.g. `self.params['mzML-files']` will give all selected mzML files."
)

st.markdown(
"""
### Reactive parameters (conditional UI)

By default every parameter widget is rendered inside an isolated `st.fragment`: changing it reruns only that widget, which keeps large parameter sections fast but also means a widget **cannot** show or hide other widgets. Pass **`reactive=True`** to `self.ui.input_widget`, `self.ui.select_input_file` or `self.ui.input_TOPP` to render the widget directly in the parent `configure()` scope instead — a change then reruns `configure()`, so you can conditionally render downstream widgets based on the current value.

Read the current value from `st.session_state` (**not** `self.params`, which is only loaded once per run and is stale within the same rerun) using the parameter-manager prefixes: `param_prefix` for `input_widget` / `select_input_file` keys, and `topp_param_prefix` for TOPP keys of the form `"<Tool>:1:<param path>"` (the `<param path>` matches the tool's ini, as shown for each widget in the panel).
"""
)
st.code(
'''@st.fragment
def configure(self) -> None:
pm = self.parameter_manager

# A custom widget that reveals more widgets when checked
self.ui.input_widget(
"advanced-mode", False, "Advanced mode",
widget_type="checkbox", reactive=True,
)
if st.session_state.get(f"{pm.param_prefix}advanced-mode", False):
self.ui.input_widget("threads", 4, "Threads", widget_type="number")

# A TOPP parameter driving conditional UI (e.g. TMT type -> channel count)
self.ui.input_TOPP("IsobaricAnalyzer", reactive=True)
iso_type = st.session_state.get(f"{pm.topp_param_prefix}IsobaricAnalyzer:1:type", "")
if iso_type.startswith("tmt"):
self.ui.input_widget("tmt-channels", 10, "TMT channels", widget_type="number")'''
)
st.info(
"💡 `reactive=True` is opt-in per widget and defaults to `False`. On `input_TOPP` it un-isolates the whole tool panel, so **any** parameter change reruns `configure()` — leave it off unless another widget's visibility depends on a value in that panel."
)

with st.expander("**Code documentation**", expanded=True):
st.help(StreamlitUI.input_widget)
st.help(StreamlitUI.select_input_file)
Expand Down
55 changes: 54 additions & 1 deletion src/workflow/StreamlitUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,6 @@ def format_files(input: Any) -> List[str]:

self.parameter_manager.save_parameters()

@st.fragment
def input_TOPP(
self,
topp_tool_name: str,
Expand All @@ -831,6 +830,7 @@ def input_TOPP(
display_subsection_tabs: bool = False,
custom_defaults: dict = {},
tool_instance_name: str = None,
reactive: bool = False,
) -> None:
"""
Generates input widgets for TOPP tool parameters dynamically based on the tool's
Expand All @@ -842,6 +842,8 @@ def input_TOPP(
num_cols (int, optional): Number of columns to use for the layout. Defaults to 3.
exclude_parameters (List[str], optional): List of parameter names to exclude from the widget. Defaults to an empty list.
include_parameters (List[str], optional): List of parameter names to include in the widget. Defaults to an empty list.
flag_parameters (List[str], optional): List of parameter names that should
be treated as no-value CLI flags during command construction.
display_tool_name (bool, optional): Whether to display the TOPP tool name. Defaults to True.
display_subsections (bool, optional): Whether to split parameters into subsections based on the prefix. Defaults to True.
display_subsection_tabs (bool, optional): Whether to display main subsections in separate tabs (if more than one main section). Defaults to False.
Expand All @@ -852,7 +854,58 @@ def input_TOPP(
defaults to topp_tool_name. The instance name is used for session
state keys and parameter storage, while topp_tool_name is used for
the actual tool executable and ini file creation.
reactive (bool, optional): If True, widget changes trigger the parent
section to re-render, enabling conditional UI based on this widget's
value. Use when downstream UI depends on a parameter value (e.g.,
TMT type driving channel count). Default is False.
"""
if reactive:
self._input_TOPP_impl(
topp_tool_name, num_cols, exclude_parameters, include_parameters,
flag_parameters, display_tool_name, display_subsections,
display_subsection_tabs, custom_defaults, tool_instance_name,
)
else:
self._input_TOPP_fragmented(
topp_tool_name, num_cols, exclude_parameters, include_parameters,
flag_parameters, display_tool_name, display_subsections,
display_subsection_tabs, custom_defaults, tool_instance_name,
)

@st.fragment
def _input_TOPP_fragmented(
self,
topp_tool_name: str,
num_cols: int = 4,
exclude_parameters: List[str] = [],
include_parameters: List[str] = [],
flag_parameters: List[str] = [],
display_tool_name: bool = True,
display_subsections: bool = True,
display_subsection_tabs: bool = False,
custom_defaults: dict = {},
tool_instance_name: str = None,
) -> None:
self._input_TOPP_impl(
topp_tool_name, num_cols, exclude_parameters, include_parameters,
flag_parameters, display_tool_name, display_subsections,
display_subsection_tabs, custom_defaults, tool_instance_name,
)

def _input_TOPP_impl(
self,
topp_tool_name: str,
num_cols: int = 4,
exclude_parameters: List[str] = [],
include_parameters: List[str] = [],
flag_parameters: List[str] = [],
display_tool_name: bool = True,
display_subsections: bool = True,
display_subsection_tabs: bool = False,
custom_defaults: dict = {},
tool_instance_name: str = None,
) -> None:
Comment on lines +895 to +907

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing implementation for flag_parameters.

The flag_parameters argument is passed into _input_TOPP_impl, but it is never used within the function body. The documented behavior of marking parameters as no-value CLI flags is currently a no-op.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 899-899: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


[warning] 900-900: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


[warning] 901-901: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


[warning] 905-905: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


[warning] 906-906: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/StreamlitUI.py` around lines 895 - 907, Implement handling for
flag_parameters within _input_TOPP_impl so each listed parameter is rendered and
submitted as a no-value CLI flag rather than requiring an input value. Preserve
the existing behavior for parameters not included in flag_parameters, and ensure
the argument is applied consistently through the function’s parameter-processing
path.

"""Internal implementation of input_TOPP - contains all the widget logic."""
# Default instance name to the tool name when not provided
if tool_instance_name is None:
tool_instance_name = topp_tool_name
Expand Down