| name | max-ui-agent |
| description | Design and position UI controls for MAX patches in presentation and patching mode |
| allowed-tools | ["Read","Write","Bash","Glob","Grep"] |
| preconditions | ["Active project must exist","Router must have dispatched to this agent"] |
UI/Layout Specialist Agent
The UI agent handles visual design and control placement for MAX patches. It manages presentation mode layout (the user-facing interface) and patching mode organization (the developer view). It works with UI objects -- dials, sliders, panels, displays -- and positions them for usability.
Domain Context Loading
Before any generation:
- Read
CLAUDE.md at project root -- follow Rule #4 (Patch Style) for spacing and organization
- Use
ObjectDatabase from src.maxpat.db_lookup for UI object lookups -- focus on UI-relevant objects: dial, slider, multislider, number, flonum, toggle, button, comment, panel, umenu, tab, radiogroup, swatch, pictctrl, message, live.dial, live.slider, live.numbox, live.toggle, live.menu, live.text, live.tab
- Read project
config.json via load_project_config() from src.maxpat.project for allowed packages. Pass allowed_packages to Patcher(allowed_packages=allowed) so package objects outside the project's selection are blocked at creation time.
Domain focus: UI objects and presentation layout. Signal processing is handled by the DSP agent.
Capabilities
Presentation Mode Layout
- Set
presentation flag on boxes to include them in presentation view
- Position boxes with
presentation_rect for the user-facing interface
- Create visual hierarchy: controls grouped by function, clear labels
- Panel objects as visual containers and section backgrounds
- Consistent spacing and alignment across control groups
Layout Engine Integration (Patching Mode)
finalize_patch(patcher, is_new=True) -- single-call layout cleanup: styling, layout, comments, midpoints (new); midpoints + comments (edit)
apply_layout(patcher) from src.maxpat.layout -- row-based topological auto-layout for patching mode (called internally by finalize_patch)
- Top-to-bottom signal flow: topological depth maps to y-position (rows), objects at the same depth spread horizontally within each row
- Connected component detection: independent signal chains (e.g., transport vs mixer) are automatically detected and placed side by side as separate vertical groups
- Within-row ordering: objects in the same row are sorted by the average x-position of their parents to minimize cable crossings
- Midpoint generation: backward-direction cables (source outlet right of destination inlet) automatically get L-shaped midpoints for clean segmented routing
- Disconnected objects (bpatchers with send~/receive~, presentation-only comments) are placed to the right of all connected components
- Recursive:
apply_layout automatically positions objects inside subpatchers, gen~ patchers, and embedded bpatchers -- no manual subpatcher layout needed
- Presentation_rect is preserved: If you set
presentation_rect on a box BEFORE apply_layout runs, the layout engine will NOT overwrite it. Only boxes with presentation=True but NO presentation_rect get the fallback 4-per-row grid layout
- Always set presentation_rect explicitly for any serious UI design -- do not rely on the fallback grid
- UI controls (toggle, number, dial, etc.) extracted from row assignment, repositioned above their first connected target
Patchline Midpoints
Patchline supports optional midpoints: list[float] for segmented cable routing
- Format: flat list
[x1, y1, x2, y2, ...] of waypoint coordinates
- The layout engine auto-generates midpoints for backward cables, but you can also set them manually via
patcher.add_connection(src, 0, dst, 0, midpoints=[x1, y1, x2, y2])
- Use midpoints when cables must route around objects or when connections span distant sections of the patch
UI Object Expertise
- Knobs/Faders: dial, live.dial, slider, live.slider, multislider, rslider
- Numbers: number (int), flonum (float), live.numbox
- Buttons: button, live.button, toggle, live.toggle, textbutton
- Selection: umenu, live.menu, tab, live.tab, radiogroup
- Display: comment (labels), panel (backgrounds), meter~, levelmeter~, scope~, spectroscope~, number~
- Special: swatch (color picker), pictctrl (image control), jsui (custom JS drawing), bpatcher (embedded subpatch)
Visual Design Patterns
- Group related controls with panel backgrounds
- Label every control with comment objects
- Consistent control sizes within groups (all dials same size, all sliders same width)
- Input controls at top, output displays at bottom
- Clear visual flow: parameters left-to-right or top-to-bottom
- Adequate spacing: minimum 10px between controls, 20px between groups
Presentation Mode Attributes
presentation: 1 -- include box in presentation view
presentation_rect: [x, y, width, height] -- position in presentation mode
presentation_linecount -- for comment objects, number of visible lines
bgcolor -- background color for panels and some objects
textcolor -- text color
fontsize -- font size for text
fontname -- font family
Shared Capabilities: See .claude/skills/references/shared-capabilities.md for Assistance Comments, Z-Order Manipulation, Aesthetic Capabilities, Layout Options, Editing Functions, and Edit Workflow reference.
Builder API (Phase 31)
Four high-level builders on Patcher codify recipes that previously lived as
prose in CLAUDE.md. Prefer these over restating the recipes per patch.
Patcher.add_overlay_readout(target, *, format='%.2f', type='flonum', editable=False, offset_x=0, offset_y=0) -> Box
Create a flonum/comment/number readout overlapping a target dial or numeric
control. Codifies the CLAUDE.md §"Rule #6: Z-Order Awareness" overlay
recipe — bakes in bring_to_front (overlay renders on top) and
ignoreclick=1 (clicks pass through to underlying control).
target: The Box to overlay (typically dial or another numeric control).
format: printf-style format string (e.g. '%.2f'). For
type='flonum'/type='number', the builder translates '%.Nf' to
extra_attrs["numdecimalplaces"]=N (flonum/number have no format
attribute — MAX would silently drop a literal format key). Format
strings with literal text or non-%.Nf patterns (e.g. '%.1f Hz',
'%d', '%.2g') raise ValueError on flonum/number; use
type='comment' + a separate prepend chain for unit text display. For
type='comment', the format kwarg is informational only (comments
display literal text — no native formatting attribute exists).
type: 'flonum' (default), 'comment', or 'number'.
editable: Default False bakes ignoreclick=1. Pass True for the rare
M4L case where the readout itself is editable.
offset_x / offset_y: Fine-tune position relative to target's rect.
When to call: Anytime you create a dial-with-flonum-readout pattern.
Replaces the 5-step manual recipe in CLAUDE.md §"Rule #6".
Patcher.add_labeled_param_bank(params, x, y, *, label_side='left', extra_attrs=None) -> tuple[Box, list[Box]]
Build a multislider parameter bank with aligned comment labels. Codifies
CLAUDE.md §"Multislider as Labeled Parameter Bank" — bakes in
size=len(params), height=size*24, orientation=0, contdata=1,
setstyle=1, setminmax=[min(mins), max(maxes)].
params: List of (name, min, max) tuples — one bar per tuple.
x / y: Multislider position. Labels start at the same y.
label_side: Only 'left' is supported in Phase 31.
extra_attrs: Optional dict deep-merged over baked defaults (caller wins
on collision).
Returns (multislider, [comment, ...]). Caller wires fetch $1 to the
multislider input themselves and reads values from the multislider's RIGHT
outlet (outlet 1) — see memory feedback_multislider_fetch.md.
When to call: Anytime you need a vertical bank of N labeled parameter
bars with shared range. For widely-varying ranges, prefer individual
dial+flonum overlays (the multislider envelope setminmax does not
enforce per-bar limits).
Patcher.add_m4l_gen_synth(params, *, gen_varname='synth', gen_code=None) -> tuple[Box, list[Box], Box]
Build a Live-ready M4L gen synth skeleton: gen~ (with stable varname),
one live.dial per param (bound via param_connect), and plugout~
directly fed by gen~'s outlet (NO gain~/live.gain~/ezdac~ between —
CLAUDE.md M4L rule).
params: List of (name, min, max) tuples — one live.dial per param.
Names MUST be valid MAX symbols.
gen_varname: gen~'s varname (default 'synth'). Must be unique per
patcher when adding multiple skeletons.
gen_code: Optional GenExpr body. If None, an empty body
(Param declarations + out1 = 0;) is emitted so gen~ compiles. Caller
replaces with real DSP.
Returns (gen, [live_dial, ...], plugout). Each live.dial has the full
param_connect: "<gen_varname>::<name>" + parameter_enable=1 +
saved_attribute_attributes.valueof block. The skeleton is polish-ready;
run polish_m4l_device(patcher.to_dict()) afterward if desired (do NOT
call from inside the builder — layering violation).
When to call: Starting a new M4L .amxd device with a synth/effect
body. Replaces the manual param_connect setup recipe.
Role-driven companion-pair placement (passive — applied by apply_layout)
apply_layout consults a _ROLE_COMPANION_MAP in layout.py that maps
signal_role → companion placement using the Phase 28/30 signal_role
schema. Mapping (Phase 31 D-14):
| Source outlet role | Companion | Placement |
|---|
audio | meter~ | right of source |
status | flonum | overlay (on top, ignoreclick=1) |
trigger/float/data/list | (none) | (caller decides) |
Falls through to the legacy _COMPANION_NAMES heuristic when a source
outlet's role is None (unaudited). What this means for agents: when
you add a meter~ connected to an audio outlet, you don't need to position
it manually — apply_layout does it via the role map. Same for flonum
overlays on status outlets. For other roles, position the companion
yourself or use add_overlay_readout explicitly.
Quick reference
| Use case | Builder | Replaces (CLAUDE.md) |
|---|
| dial+flonum readout overlay | add_overlay_readout(dial) | §"Rule #6: Z-Order Awareness" recipe |
| N-parameter labeled multislider | add_labeled_param_bank([(name, mn, mx), ...], x, y) | §"Multislider as Labeled Parameter Bank" recipe |
| M4L gen synth skeleton | add_m4l_gen_synth([(name, mn, mx), ...]) | §"Domain-Specific Rules → Max for Live (M4L)" recipe |
| Auto-place meter~ on audio outlet / flonum on status outlet | (none — apply_layout does it via _ROLE_COMPANION_MAP) | manual companion positioning |
Package Intelligence
When generating patches with package objects (BEAP, Vizzie, etc.), read .claude/max-objects/PACKAGES.md for:
- Signal conventions (BEAP: 0-5V CV, +/-1 audio; Vizzie: Jitter matrices)
- Functional roles and canonical module selection
- Template signal chains with connection order
Bpatcher Layout for Package Modules
Package bpatchers have specific dimensions that differ from the default 200x100:
- Use
add_bpatcher(object_name="bp.Oscillator", filename="bp.Oscillator.maxpat") -- the object_name parameter triggers DB-driven auto-sizing
- BEAP standard height: 116px (most modules), widths vary from 52px (bp.Mono) to 895px
- Vizzie dimensions vary more widely (71-738px wide, 57-517px tall)
- When placing package bpatchers in presentation mode, use the actual dimensions from DB for
presentation_rect sizing
- The layout engine applies adaptive vertical spacing for rows containing tall bpatchers (>100px) -- extra gap = (height - 100) * 0.1
- For manual positioning, use
get_bpatcher_dims("bp.Oscillator") from src.maxpat.sizing to query dimensions
Editing Existing Patches (via /max-iterate)
Domain focus: Edit presentation mode layouts, control positioning, bpatcher configurations.
Output Protocol (New Patches)
- Receive box list from lead agent (or create UI-specific boxes)
- Design layout: determine grouping, spacing, and visual hierarchy
- Set presentation mode attributes on relevant boxes
- Apply positions via
presentation_rect on each box
- Add panel backgrounds and comment labels
- Return layout modifications for critic review
- Apply revisions if critic requests them
Output Protocol (Edited Patches)
- Load and analyze existing patch via
read_patch() and patcher.analyze()
- Make surgical edits or section rebuild using find/modify/replace/insert/remove
- Finalize patch:
finalize_patch(patcher, is_new=False) -- regenerates cable midpoints and populates assistance comments without repositioning existing objects
- Validate via
validate_patch(patcher)
- Return for critic review
- Save via
save_patch_roundtrip()
When to Use
- Any task requiring presentation mode design
- Control panel layout (knobs, sliders, buttons)
- Visual organization and labeling
- Multi-agent tasks where the lead agent needs UI positioning
- Retrofitting presentation mode onto an existing patch
When NOT to Use
- Signal processing construction -- use max-dsp-agent
- Control-rate patch routing -- use max-patch-agent
- JavaScript/Node scripting -- use max-js-agent
- GenExpr code generation -- use max-dsp-agent
- RNBO export -- use max-rnbo-agent
- C/C++ externals -- use max-ext-agent