fix(ui): auto-create agent config entry so agents-page edits mark form dirty#39326
fix(ui): auto-create agent config entry so agents-page edits mark form dirty#39326dunamismax wants to merge 1 commit intoopenclaw:mainfrom
Conversation
…m dirty
When the selected agent has no explicit entry in agents.list (e.g. the
default 'main' agent inheriting global/defaults settings), every change
callback in app-render.ts bailed out at the 'find agent index' step
because there was no matching list entry to update. The config form was
never mutated, configFormDirty was never set, and the Save button stayed
permanently disabled.
Add findAgentConfigEntryIndex() and ensureAgentConfigEntry() helpers in
controllers/config.ts. ensureAgentConfigEntry seeds a minimal { id }
entry via updateConfigFormValue when the agent is not yet in the list,
which correctly marks the form dirty and triggers a Lit re-render.
All seven agents-page change callbacks (onModelChange,
onModelFallbacksChange, onToolsProfileChange, onToolsOverridesChange,
onAgentSkillToggle, onAgentSkillsClear, onAgentSkillsDisableAll) now use
these helpers instead of inline list-search + early-return guards.
Fixes #39268
Greptile SummaryThis PR fixes a real and impactful bug: agent-page settings changes (model, tool profile, overrides, skills) were silently discarded for inherited agents because the change callbacks bailed out early when no explicit Key changes and their correctness:
Two issues worth addressing:
Tests cover the three critical paths (find, create-stub, no-duplicate) and are well-written. Confidence Score: 4/5
|
| export function ensureAgentConfigEntry( | ||
| state: ConfigState, | ||
| agentId: string, | ||
| config?: Record<string, unknown> | null, | ||
| ): number { | ||
| const normalizedAgentId = agentId.trim(); | ||
| if (!normalizedAgentId) { | ||
| return -1; | ||
| } | ||
| const source = | ||
| config ?? state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null); | ||
| const existingIndex = findAgentConfigEntryIndex(source, normalizedAgentId); | ||
| if (existingIndex >= 0) { | ||
| return existingIndex; | ||
| } | ||
| const list = (source as { agents?: { list?: unknown[] } } | null)?.agents?.list; | ||
| const nextIndex = Array.isArray(list) ? list.length : 0; | ||
| updateConfigFormValue(state, ["agents", "list", nextIndex, "id"], normalizedAgentId); | ||
| return nextIndex; | ||
| } |
There was a problem hiding this comment.
config parameter vs. state.configForm precedence
source is computed as config ?? state.configForm ?? .... In app-render.ts, this is called as:
const ensureAgentIndex = (agentId) => ensureAgentConfigEntry(state, agentId, configValue);where configValue = state.configForm ?? state.configSnapshot?.config ?? null.
Because configValue is captured at render time, it is equivalent to state.configForm (or the snapshot when the form is clean). This means the config parameter is functionally redundant — passing it doesn't change source relative to what the function would compute on its own.
The concern is that passing a render-time snapshot as config bypasses the "check state.configForm first" protection. If, hypothetically, two edits occurred synchronously without a Lit re-render between them, the second call's configValue would not reflect the entry created by the first call, and ensureAgentConfigEntry would calculate nextIndex from the stale list length (potentially duplicating the entry at index 0).
In practice this does not occur today because each user interaction fires a single callback before the next Lit render. However, to make the function more robust, consider dropping the config parameter and always resolving source from state.configForm ?? state.configSnapshot?.config internally, so the guard against duplicate creation is always authoritative:
export function ensureAgentConfigEntry(
state: ConfigState,
agentId: string,
): number {
...
const source = state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
...
}This would also align the signature with how the function is tested (called without a third argument).
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/controllers/config.ts
Line: 242-261
Comment:
**`config` parameter vs. `state.configForm` precedence**
`source` is computed as `config ?? state.configForm ?? ...`. In `app-render.ts`, this is called as:
```ts
const ensureAgentIndex = (agentId) => ensureAgentConfigEntry(state, agentId, configValue);
```
where `configValue = state.configForm ?? state.configSnapshot?.config ?? null`.
Because `configValue` is captured at render time, it is equivalent to `state.configForm` (or the snapshot when the form is clean). This means the `config` parameter is functionally redundant — passing it doesn't change `source` relative to what the function would compute on its own.
The concern is that passing a render-time snapshot as `config` bypasses the "check `state.configForm` first" protection. If, hypothetically, two edits occurred synchronously without a Lit re-render between them, the second call's `configValue` would not reflect the entry created by the first call, and `ensureAgentConfigEntry` would calculate `nextIndex` from the stale list length (potentially duplicating the entry at index 0).
In practice this does not occur today because each user interaction fires a single callback before the next Lit render. However, to make the function more robust, consider dropping the `config` parameter and always resolving `source` from `state.configForm ?? state.configSnapshot?.config` internally, so the guard against duplicate creation is always authoritative:
```ts
export function ensureAgentConfigEntry(
state: ConfigState,
agentId: string,
): number {
...
const source = state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
...
}
```
This would also align the signature with how the function is tested (called without a third argument).
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 08951026d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| entry && | ||
| typeof entry === "object" && | ||
| "id" in entry && | ||
| (entry as { id?: string }).id === normalizedAgentId, |
There was a problem hiding this comment.
Match agent IDs using canonical normalization
findAgentConfigEntryIndex now treats IDs as an exact string match against agentId.trim(), but the UI receives normalized IDs from agents.list while existing config entries can still be non-canonical (for example mixed-case IDs from manual/legacy config). In that case this lookup returns -1, ensureAgentConfigEntry appends a new {id} row, and subsequent edits can be written to the duplicate entry instead of the original one. Because runtime resolution uses normalized first-match lookup (src/agents/agent-scope.ts:114), those GUI edits may be ignored after save for affected configs.
Useful? React with 👍 / 👎.
Landed from contributor PR #39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
|
Rebased onto latest What changed on top of the original PR:
Landed commit: 49261b0 Thanks @dunamismax. |
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]>
Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]> (cherry picked from commit 49261b0)
) * Secrets: refresh baseline for tts line drift (cherry picked from commit 0018f47) * fix: auto-create inherited agent override entries Landed from contributor PR openclaw#39326 by @dunamismax. Co-authored-by: dunamismax <[email protected]> (cherry picked from commit 49261b0) * fix: preserve agents-page selection after config save Landed from contributor PR openclaw#39301 by @MumuTW. Co-authored-by: MumuTW <[email protected]> (cherry picked from commit c0a7c30) * Agents UI: compose save state from config state (cherry picked from commit 96f4f50) * Agents UI: complete config state test fixture (cherry picked from commit 0125bd9) * Secrets: refresh baseline for model provider docs (cherry picked from commit 14916fb) * docs: dedupe changelog contributor attribution (cherry picked from commit 75a44de) * build: reduce build log noise (cherry picked from commit dd8fd98) * build: prepare 2026.3.7-beta.1 release (cherry picked from commit 3596a46) * fix(ui): align control-ui device auth token signing (cherry picked from commit e0f80cf) * build: prepare 2026.3.7 release (cherry picked from commit 42a1394) --------- Co-authored-by: Vincent Koc <[email protected]> Co-authored-by: Peter Steinberger <[email protected]> Co-authored-by: dunamismax <[email protected]> Co-authored-by: MumuTW <[email protected]>
Summary
findAgentConfigEntryIndex()andensureAgentConfigEntry()helpers incontrollers/config.ts. All seven agents-page change callbacks inapp-render.tsnow use these helpers instead of inline list-search + early-return guards. When the selected agent has no explicitagents.listentry (e.g. the default "main" agent inheriting global/defaults),ensureAgentConfigEntryseeds a minimal{ id }entry viaupdateConfigFormValue, which correctly marks the form dirty and triggers a Lit re-render.updateConfigFormValue,removeConfigFormValue,@state() configFormDirty) was already correct — the bug was that callbacks never reached those functions for inherited agents.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
agents.listentry, the first edit auto-creates a minimal{ id: "<agentId>" }entry in the config form. This entry is written to disk only when the user clicks Save.Security Impact (required)
NoNoNoNoNoRepro + Verification
Environment
agents.listentriesSteps
Expected
Save button enables after any change; config persists on save.
Actual (before fix)
Save button stays permanently grayed out; changes silently lost.
Evidence
New tests added in
controllers/config.test.ts:finds explicit agent entries— verifiesfindAgentConfigEntryIndexlocates existing entriescreates an agent override entry when editing an inherited agent— verifiesensureAgentConfigEntryseeds{ id }, setsconfigFormDirty = truereuses the existing agent entry instead of duplicating it— verifies no double-create when entry already existsAll checks pass:
pnpm check(format + tsgo + lint + all lint gates) ✅pnpm build✅pnpm ui:build✅pnpm --dir ui test(targeted: config.test.ts + agents-panels-tools-skills.browser.test.ts — 16 tests) ✅Human Verification (required)
onModelChange,onModelFallbacksChange,onToolsProfileChange,onToolsOverridesChange,onAgentSkillToggle,onAgentSkillsClear,onAgentSkillsDisableAll) now reachupdateConfigFormValue/removeConfigFormValuefor both inherited and explicit agents.agents.listat all (empty config); agent already in list (no duplicate entry created); clearing model on inherited agent (no-op when entry doesn't exist, clean removal when it does).Compatibility / Migration
YesNoNoFailure Recovery (if this breaks)
ui/src/ui/app-render.ts,ui/src/ui/controllers/config.ts,ui/src/ui/controllers/config.test.ts{ id }entry causes unexpected config serialization, the config file would gain a newagents.listarray entry after saving — this is correct behavior (it is the explicit override the user intended), but reviewers should confirm it does not produce invalid config shapes.Risks and Mitigations
agents.listwith a minimal{ id }entry that the gateway config validator rejects.updateConfigFormValue(the existing mechanism used for all other config edits) handles form serialization and coercion. The{ id }stub is the minimal valid shape. The validator is tested separately and accepts entries with onlyidset.agents.listentry, thefindAgentConfigEntryIndexlookup could mismatch.entry.id === agentIdcomparison. New unit test confirms correct index resolution.