Skip to content

Feat(GUI3): update preset while loaded (#2356)#2429

Merged
fl0rianr merged 2 commits into
kpoin/ui-testingfrom
feat/gui3-update-preset-while-loaded
Jun 26, 2026
Merged

Feat(GUI3): update preset while loaded (#2356)#2429
fl0rianr merged 2 commits into
kpoin/ui-testingfrom
feat/gui3-update-preset-while-loaded

Conversation

@kpoineal

Copy link
Copy Markdown
Collaborator

Part of #2356.

What shipped (UI / POC)

Adds an "Update preset" action to the model detail panel that appears next to Unload when a model is loaded and a different preset is linked to it (linked ≠ running):

  • Live-updateable changes (sampling, system prompt, tools toggle) apply without a reload — inline "applied live, no reload needed" status.
  • Reload-requiring changes (recipe_options such as ctx_size/backend/device/args/steps, or the engine hint) trigger an automatic reload flow with a "Reloading…" spinner/status — the user never manually unloads/loads.
  • Client-local running-preset tracking (running_presets store) + classifyPresetChange(running, next) → 'none' | 'live' | 'reload' in presetStore.ts.
  • A clean api.updatePreset(modelName, presetId, mode, payload) hook against the proposed POST /api/v{0,1}/update-preset contract.

Accessibility

  • Real <button>; aria-label names the model and states "(reloads the model)" for reload changes; aria-busy while updating.
  • Always-present role="status" aria-live="polite" region announces the outcome; sighted aria-hidden hint explains why the button appeared.
  • Focus moves to Unload on success (the button unmounts once running == linked) to avoid focus loss.
  • Reload spinner respects prefers-reduced-motion.
  • Playwright/axe coverage A154–A163 (10 new tests). All 178 tests pass, 7 skipped, 0 failed.

Deferred to backend (lemond is off-limits to the UI POC)

  • The real reload mechanics (lemond reinitializing the model in place).
  • The Lemonade-tools "change the preset" integration (must reuse the same update logic for UI/tool parity).
  • Proposed window.api + HTTP contract and the live-vs-reload field split are documented in prototype/ui-redesign/docs/UPDATE_PRESET_CONTRACT.md@fl0rianr please confirm the contract + the field classification.

Acceptance criteria satisfied in the POC

  • Loaded model no longer needs manual unload→reload to change preset
  • Selecting a different preset on a loaded model enables Update preset
  • Update preset sits next to Unload
  • Live changes (system prompt / sampling) apply without reload
  • Reload-requiring changes trigger the automatic reload flow
  • SR roles/names/states, keyboard operability, axe coverage, all tests pass

Tool-parity + real-reload criteria are deferred to backend (noted above).

Add an 'Update preset' action to the model detail panel that appears next to Unload when a loaded model has a different preset linked than the one it is running. Live-updateable changes (sampling, system prompt) apply without a reload; reload-requiring changes (recipe_options, engine hint) trigger an automatic reload flow with progress/status — no manual unload/load.

Adds client-local running-preset tracking and classifyPresetChange (live/reload/none) in presetStore, an api.updatePreset() hook against the proposed POST /api/v{0,1}/update-preset contract, polite aria-live feedback, focus management, and Playwright/axe coverage A154-A163. Real reload mechanics and the Lemonade-tools 'change the preset' wiring are deferred to the backend (lemond is off-limits to the UI POC); contract documented in docs/UPDATE_PRESET_CONTRACT.md.

Part of #2356

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kpoineal kpoineal added this to the GUI3 milestone Jun 26, 2026
@kpoineal kpoineal added the squad:mattingly Assigned to ⚛️ Mattingly (UI / Frontend) label Jun 26, 2026
@github-actions github-actions Bot added app enhancement New feature or request labels Jun 26, 2026
@kpoineal

Copy link
Copy Markdown
Collaborator Author

@fl0rianr — whenever you have a moment, could you take a look at this PR for #2356? It's the UI/POC for update-preset-while-loaded (Update button next to Unload, live-vs-reload paths, full a11y, 178/7/0 tests green).

I'd love your guidance on three things before the backend team wires up the real reload mechanics:

  1. Endpoint shape — dedicated POST /api/v{0,1}/update-preset vs. an update_preset flag on the existing load call?
  2. Live-vs-reload field split — does this look right?
    • reload: recipe_options (ctx_size, backend, device, args, steps, cfg…), engine_hint
    • live: sampling (temperature/top_p/top_k/repeat_penalty), system_prompt, tools_enabled
  3. preset_id resolution — should preset_id stay opaque to lemond (UI resolves the fields) or should lemond resolve presets from a shared registry?

Full proposed contract is in prototype/ui-redesign/docs/UPDATE_PRESET_CONTRACT.md. No rush — just flagging it's ready for review whenever you're free. Thanks!

@fl0rianr

Copy link
Copy Markdown
Collaborator

I think the current API split is too complicated and puts the wrong responsibility on the UI. We probably do not need an update-preset endpoint with a client-provided mode: "live" | "reload".

For request-time fields like system prompt, sampling/temperature and probably tools, the frontend can simply update the active preset binding and send those values with the next generation request. No model runtime state has to change and no backend reload is needed. Not at this point.

For load-time fields like ctx_size, backend, device or model args, we need a real reload anyway. In that case the backend should either use the existing unload/load. The UI should not be the source of truth for runtime capability. You can name a reload function for a potential future backend change, but currently just execute a unload and a load afterwards like on main.

Tools need adapting:
ui-testing/prototype/ui-redesign/src/tools/lemonadeTools.ts
My idea is a change preset call which leads to the preset update workflow directly.

Also, the current classifyPresetChange returns none when running.id === next.id, so editing an existing preset with the same ID will not surface an update even if temperature, system prompt or ctx changed. That seems like a correctness bug.

Suggested simplification:

UI stores linkedPresetId / active preset binding.
Request-time fields are applied by request composition in the frontend.
Load-time fields mark the loaded model as “needs reload”.
The button can say “Apply preset” for live-only changes and “Reload to apply preset” for load-time changes.
No public mode parameter is needed; if backend support is required, send the desired config and let backend classify/validate.

I would appreciate lovell has a look as well before coming back to me.

@kpoineal

Copy link
Copy Markdown
Collaborator Author

@fl0rianr — reviewed per your request. I agree with the simplification; it removes an endpoint we don't need and puts each field group where it already belongs.

Confirmed: the classifyPresetChange correctness bug. In prototype/ui-redesign/src/presetStore.ts:570, if (running.id === next.id) return 'none'; short-circuits before the RELOAD_FIELDS/LIVE_FIELDS comparisons (lines 571–576). So editing a preset in place (same id, changed temperature / system_prompt / ctx_size) classifies as none, no Apply/Reload affordance appears, and the running model keeps stale values. Fix: drop that early return and let the field comparisons run regardless of id; return none only when no field in either group actually differs.

Plan (Mattingly implements on this branch, tests stay green):

  • Drop the update-preset endpoint, the mode param, and api.updatePreset(...). UI keeps a per-model linkedPresetId active-preset binding (client-local, invariant Request to you for benchmark report #11).
  • Request-time fields (system_prompt, sampling, tools) → applied by request composition on the next generation request (already wired: samplingForModel in api.ts, systemPromptTextForPreset in ChatView.tsx). Rebinding the preset is the whole live operation — nothing is POSTed.
  • Load-time fields (ctx_size, backend, device, model args) → mark the model "needs reload" and run reloadModel() = unload + load (like main). reloadModel is just a named wrapper so a future in-place backend reload can drop in without touching callers.
  • Fix classifyPresetChange per above; button label is "Apply preset" (live) vs "Reload to apply preset" (load-time).
  • lemonadeTools.ts: add a change_preset tool that drives this same workflow directly (resolve+compatibility-check preset, rebind, then live no-op or reloadModel), reusing the existing resolvePreset/isCompatible/applyPresetBinding helpers from load_model.
  • a11y: updated/new controls get aria-labels; existing A154–A163 are reworked to the no-mode flow and new coverage lands at A164+.

lemond stays untouched (POC constraint). Contract doc updated in this branch (prototype/ui-redesign/docs/UPDATE_PRESET_CONTRACT.md) to reflect the above; Mattingly will commit the code + doc together and keep npx playwright test green.

Blocking design questions:

  1. Should the active-preset binding persist across the unload/reload so the reloaded model comes back running the new preset (UI re-binds before reloading) — and same for the tool path?
  2. For request-time tool changes: confirm tools are purely request-composition for every backend (composed alongside sampling + system prompt, no reload) — i.e. nothing tool-related is load-time.

— Lovell

@fl0rianr

Copy link
Copy Markdown
Collaborator

Regarding the blocking design questions:

  1. Yes. The active-preset binding should persist across unload/reload. The UI should bind the new preset first, then reload so the model comes back running that preset. Same for the tool path.

  2. Yes, please treat tools as request-time composition together with sampling and system prompt. No reload should be required for tool changes, unless we later discover a backend-specific exception.

…PresetChange (#2356)

Per @fl0rianr review + Lovell's plan:

- Fix classifyPresetChange: remove early same-id 'none' return so in-place edits classify as live/reload correctly

- Remove api.updatePreset and the client mode param; add reloadModel() helper (unload+load)

- Live (request-time) preset changes are a pure client-local rebind via request composition; no POST

- Load-time changes reload the model; active-preset binding persists across reload

- Add change_preset Lemonade tool driving the same workflow

- Button labels: 'Apply preset' (live) / 'Reload to apply preset' (load-time)

- Tests A154-A166 reworked/added; full suite 181 passed, 7 skipped

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kpoineal

Copy link
Copy Markdown
Collaborator Author

@fl0rianr — implemented the simplified "update preset while loaded" design per your review feedback + Lovell''s plan. Pushed to PR #2429.

What changed

  • classifyPresetChange correctness fix: removed the early running.id === next.id → 'none' return. Editing a preset in place (same id, changed temperature/system_prompt/ctx_size) now classifies as live/reload correctly; none is returned only when no reload- or live-field actually differs.
  • Dropped the endpoint + mode abstraction: removed api.updatePreset(...) and the client-provided mode:'live'|'reload' framing. No public mode parameter, no /update-preset POST.
  • Per-model linkedPresetId binding stays client-local (invariant Request to you for benchmark report #11 — never in lemond).
  • Live (request-time) fields (system_prompt, sampling/temperature, tools): rebinding the active preset is the whole op — request composition (samplingForModel, systemPromptTextForPreset) carries them on the next request. No POST, no reload.
  • Load-time (reload) fields (ctx_size, backend, device, model args): new named reloadModel() helper = unload + load today (named for a future in-place backend reload).
  • change_preset Lemonade tool drives the same workflow directly (resolve + isCompatible, applyPresetBinding, then live no-op or reloadModel).
  • Button labels: "Apply preset" (live-only) / "Reload to apply preset" (load-time); aria-labels name the target ("Apply preset for {name}" / "Reload {name} to apply preset").

Assumption I made on the open question (please confirm): the active-preset binding persists across the unload/reload — the UI rebinds before reloading so the reloaded model comes up running the newly-bound preset, and the change_preset tool path behaves identically. Documented in code comments + UPDATE_PRESET_CONTRACT.md.

Tests: reworked A154–A163 to the no-endpoint/no-mode flow and added A164–A166. Full Playwright suite: 181 passed, 7 skipped, 0 failed.

— Mattingly

@kpoineal

Copy link
Copy Markdown
Collaborator Author

@fl0rianr — thanks for confirming. I verified the shipped code matches both of your answers; no fix-up commit was needed (already correct as of 96739e1).

(a) Load-time changes bind the new preset FIRST, then reload.

  • Tool path (src/tools/lemonadeTools.ts, change_preset): applyPresetBinding(targetModelName, nextPreset) runs (line 846) before await api.reloadModel(...) (line 849). The running-preset snapshot is recorded only after a successful reload.
  • UI path (src/components/ModelDetailPanel.tsx, handleUpdatePreset): the active-preset binding (linked = activePresetForModel) is already in place before the handler runs (user re-links via the chooser), and the reload is performed with linked.recipe_options, so the model comes back running the newly-bound preset. api.reloadModel = unload→load (src/api.ts).

(b) Tools are request-time composition (live, no reload), alongside sampling + system prompt.

  • src/presetStore.ts: tools_enabled is in LIVE_FIELDS (with sampling, system_prompt_id, system_prompts), NOT in RELOAD_FIELDS (recipe_options, engine_hint). So a tools-only change classifies as 'live' in classifyPresetChange — applied on the next request, no reload. Matches your note that tools need no reload unless a backend-specific exception emerges.

(c) Fix-up commit: none required — the ordering and tools-live classification were already correct as shipped.

(d) Tests: full Playwright suite green — 181 passed, 7 skipped, 0 failed.

Ready for your review / merge.

— Mattingly

@fl0rianr fl0rianr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM now

@fl0rianr fl0rianr merged commit ac3b0c7 into kpoin/ui-testing Jun 26, 2026
54 of 56 checks passed
@fl0rianr fl0rianr deleted the feat/gui3-update-preset-while-loaded branch June 26, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app enhancement New feature or request squad:mattingly Assigned to ⚛️ Mattingly (UI / Frontend)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants