Agent Preset Control Panel
Purpose
Space Agent can currently act only through presets resolved from the current interaction context. That works for local context-sensitive assistance, but it blocks legitimate cross-context work. For example, the user may be on the drawing canvas and still want Space Agent to inspect graph nodes, manage scene objects, or run a file search.
The preset control panel exposes the preset system directly in the Agent panel so the user can choose which capabilities Space Agent has right now.
This is not a replacement for tool approvals. Preset selection is a user action and can be trusted. Tool execution is a model action and still needs approval policy enforcement for risky operations.
Existing Model
The agent layer is split into three responsibilities:
- Agents own turn orchestration.
AgentRunnerloads a session, resolvessession.agent-id, builds the run context, and callsagent.run. - Presets own capability selection.
PresetRegistryresolves active presets from context and overrides. - Tool surfaces own executable tool exposure.
AgentToolSurfaceconverts resolved preset tool IDs into tool definitions and applies approval policy before execution.
Current preset shape:
{:name "drawing-shape-tools"
:group "drawing"
:default-state :auto
:risk :normal
:contexts [{:surface :canvas :mode "drawing"}]
:tool-ids ["drawing.inspect" "drawing.set-tool"]
:system-prompt "Use drawing tools for shape and stroke operations."}Current override states:
:auto: context decides.:on: preset is active regardless of context.:off: preset is inactive regardless of context.
These semantics are already implemented in PresetRegistry.resolve and PresetManager.set-override.
Trust Boundary
The panel must preserve this distinction:
- User-selected preset override: trusted control-plane state.
- Model-selected tool call: untrusted action request.
Therefore:
- Toggling a preset does not require confirmation.
- Toggling a preset does not auto-run any tool.
- Risk labels are visible, but never block preset toggles.
- Existing approval behavior remains for tool calls chosen by the model.
Example:
- User pins
general-shell-toolstoon. - Space Agent now sees shell capability.
- Model decides to call
space_app_run_bash. - Approval policy still handles that tool execution because the action was chosen by the model.
User Experience
Add a Presets area to the Agent panel.
The user should be able to:
- See all registered presets, including inactive context-mismatched presets.
- See which presets are active.
- See why each active preset is active: context or override.
- Pin any preset
on. - Force any preset
off. - Return any preset to
auto. - See risk level and tool count before changing state.
- Reset all overrides.
No confirmation dialogs or warnings are shown when changing states.
Layout
Recommended panel structure:
- Agent selector.
- Status row.
- Agent tabs:
SessionsPresets
- Transcript.
- Pending approval row.
- Input row.
Tabs avoid shrinking the transcript while still making presets easy to find.
Initial implementation can use a simpler inline section above sessions if adding tabs is more work than justified. The controller/view model should be independent of the final layout so the UI can move later.
Preset Row
Each preset row contains:
- Name.
- Group.
- Three-state segmented control:
Auto,On,Off. - Active badge:
active,inactive, orpinned. - Risk badge.
- Tool count.
- Optional expanded tool ID list.
State labels:
auto active: no override, context matched.auto inactive: no override, context did not match.pinned on: override:on, active because of user.forced off: override:off, inactive because of user.
Risk display:
normal: neutral.filesystem-read: info.filesystem-write: warning.destructive: danger.shell: danger.
Risk is informational only in this panel.
Controller View Model
AgentPanelController should receive :presets.
(AgentPanelController {:runner opts.runner
:registry opts.registry
:approvals opts.approvals
:presets opts.presets})Extend controller state:
{:preset-rows []
:preset-groups []
:expanded-preset-groups {}
:expanded-presets {}}Computed row shape:
{:name preset.name
:group (or preset.group "other")
:risk preset.risk
:default-state preset.default-state
:contexts preset.contexts
:tool-ids preset.tool-ids
:tool-count (# preset.tool-ids)
:override-state (or override.state :auto)
:active? active?
:active-reason active.reason}Controller methods:
:load-presets
:set-preset-override
:reset-preset-overrides
:toggle-preset-expanded
:toggle-preset-group-expanded
:is-preset-expanded?
:is-preset-group-expanded?load-presets:
- Read all presets from
presets.registry:list. - Read active presets from
presets:get-active-presets. - Read overrides from
presets:get-overrides. - Build rows grouped by
group. - Sort groups by name.
- Sort rows by name inside each group.
- Store rows in controller state.
- Emit controller change signal.
set-preset-override:
(fn set-preset-override [self name state]
(presets:set-override name state)
(self:load-presets))reset-preset-overrides:
(each [name _override (pairs (presets:get-overrides))]
(presets:set-override name :auto))The reset operation should not unregister presets or modify context.
Change Propagation
AgentPanelController should subscribe to presets:add-on-change during init.
On preset change:
- Recompute preset rows.
- Refresh active state and override labels.
- Let existing MCP sync/tool surface listeners update active tool definitions.
On drop:
- Remove preset change listener.
- Clear controller state.
The preset manager already emits changes only when the effective signature changes. The panel can still call load-presets after direct user changes so UI state remains immediate even when a state change does not alter effective active tools.
Persistence
Initial implementation should use in-memory overrides only, matching current PresetManager behavior.
Do not add persistence in the same change unless required by product work. Persistence needs a separate decision:
- App-global overrides.
- Per-agent overrides.
- Per-session overrides.
- Per-world overrides.
Recommended later default: app-global overrides saved atomically with JsonUtils.write-json!. The user is controlling Space Agent's capability surface, so app-global behavior is easiest to understand.
Prompt and Tool Effects
When a preset becomes active:
- Its tool IDs resolve through
PresetManager:get-tool-defs. - Its prompt fragment appears in
AgentToolSurface:prompt-fragments. - Space Agent includes active preset names in
Available Capabilities. - Space Agent includes preset guidance in
Capability Guidance.
No additional Space Agent changes are required for basic preset control.
Approval Effects
Preset toggles must not call approvals.
Approvals remain only at model-action time:
AgentToolSurface:callchecks active tool.run-with-approvalrequests risk approval for tool arguments.space_agent_request_tool_approvalremains available for the model to request approval before risky calls.
This keeps the policy coherent: user can grant capability exposure, but model cannot silently execute risky actions.
Failure Behavior
The UI should fail loudly for invalid state.
- Unknown preset name: surface the
PresetManager:set-overrideerror. - Invalid override state: surface the error.
- Missing
opts.presets: assert inAgentPanelandAgentPanelController. - Missing preset registry access: assert when building rows.
Do not silently hide broken presets. A broken capability surface is a product bug, not a user preference.
Implementation Plan
1. Controller Support
Update assets/lua/llm/agent/ui/controller.fnl.
Add required opts.presets.
Add state fields:
:preset-rows []
:preset-groups []
:expanded-preset-groups {}
:expanded-presets {}Add listener lifecycle:
(var preset-change-listener nil)In init, connect:
(set preset-change-listener
(presets:add-on-change
(fn []
(self:load-presets))))In drop, disconnect with presets:remove-on-change.
Add methods listed above and include them in the returned controller table.
2. Preset List Widget
Add assets/lua/llm/agent/ui/preset-list.fnl.
Pattern after session-list.fnl:
- Use
ListView. - Header:
Presetsplus reset button. - Rows grouped by
group. - Row click can expand/collapse details.
Auto,On,Offbuttons callcontroller:set-preset-override.- Expanded row shows contexts and tool IDs.
Controls:
- Use icon buttons where appropriate.
- Use text buttons for
Auto,On,Offbecause they are state labels, not generic icon actions. - Keep rows compact.
3. Panel Integration
Update assets/lua/llm/agent/ui/panel.fnl.
Requirements:
- Assert
opts.presets. - Pass
:presetsintoAgentPanelController. - Build
AgentPresetList. - Include preset widget in layout.
First pass layout:
(local preset-list (AgentPresetList controller))
(local preset-list-widget (preset-list ctx))
(local sections [(FlexChild (fn [_ctx] agent-row-padded))
(FlexChild (fn [_ctx] status-row-padded))
(FlexChild (fn [_ctx] preset-list-widget) 0)
(FlexChild (fn [_ctx] session-list-widget) 0)
(FlexChild (fn [_ctx] transcript-widget) 1)
...])If height becomes cramped, add tabs in a follow-up instead of overfitting the first patch.
Refresh path:
(preset-list-widget:refresh)Drop path:
(preset-list-widget:drop)4. App Wiring
Find current AgentPanel construction site and pass the existing PresetManager instance as :presets.
Do not create another preset manager for the panel. The panel must control the same manager used by:
- Agent runner context.
- Agent tool surface.
- MCP sync.
5. Tests
Add or extend Fennel tests.
Controller tests:
- Controller requires
:presets. load-presetsincludes inactive presets.- Context-active preset row reports
active? trueandactive-reason :context. set-preset-override name :onactivates context-mismatched preset.set-preset-override name :offdeactivates context-matched preset.set-preset-override name :autoreturns to context behavior.reset-preset-overridesreturns all overrides to:auto.- Preset manager change listener refreshes rows.
dropremoves preset listener.
Preset list widget tests:
- Build with dummy context.
- Refresh after controller state changes.
- Toggle buttons call controller override method.
- Expanded details show tool IDs.
Integration tests:
- Pin graph preset on while context is drawing.
- Verify graph tool IDs resolve through
PresetManager:get-tool-ids. - Verify
AgentToolSurface:mcp-tool-defsincludes graph tools. - Verify risky tool still goes through approval when called.
Recommended direct test command:
SKIP_KEYRING_TESTS=1 XDG_DATA_HOME=/tmp/space/tests/xdg-data SPACE_DISABLE_AUDIO=1 SPACE_ASSETS_PATH=$(pwd)/assets make testNon-Goals
This design does not:
- Add a router agent.
- Add sub-agent orchestration.
- Persist preset overrides.
- Remove tool approvals.
- Add per-session capability profiles.
- Add model-driven preset selection.
Those are separate features.
Open Questions
- Should overrides eventually be app-global, per-agent, per-session, or per-world?
- Should the UI show raw tool IDs by default or only when expanded?
- Should risky presets sort to the bottom of each group?
- Should there be a one-click
All graph,All scene,All drawinggroup state control?
The initial implementation can defer these and keep the contract small: every preset gets Auto, On, and Off.
