Skip to content

Use semantic Provider dirty detection when edits are reverted #84

Closed

Description

@NOirBRight

Summary

Provider dirty detection compares the raw draft and normalized Provider using JSON.stringify. Editing a model and then restoring the visible value can still leave hidden/default fields in the draft, so the Save button remains enabled and switching Providers incorrectly opens the unsaved-changes dialog even though the user made no material change.

Reproduction

Observed in the portable build from commit 7257701bf4c5d8be6fd3f49da0b0d86690dc4180.

  1. Open an existing Provider whose model omits optional/default metadata fields.
  2. Change a visible model value, such as the enabled toggle.
  3. Change it back to its original value.
  4. Observe that Save can remain enabled.
  5. Select another Provider and observe the unsaved-changes dialog.

Root cause evidence

ProviderDetail currently computes:

const dirty = JSON.stringify(draft) !== JSON.stringify(normalizedProvider);

Model edits run through normalizeModel, which materializes otherwise omitted values including:

  • context_window: null
  • input_modalities: ["text"]
  • supported_reasoning_levels: []
  • default_reasoning_level: null

A deterministic replay shows the visible enabled value restored to its original value while dirty remains true solely because those hidden keys were added.

Expected behavior

Dirty state should represent a material, persistable configuration difference, not object shape, key order, omitted defaults, or normalization side effects.

Acceptance criteria

  • Introduce one canonical Provider comparison/serialization boundary used by Save enablement and the navigation guard.
  • Normalize both the persisted baseline and draft, including nested models, before comparison.
  • Treat omitted fields and their persisted default equivalents as equal.
  • Preserve meaningful ordering differences where order affects model priority.
  • Reverting every visible edit disables Save and allows Provider navigation without a confirmation dialog.
  • Add regression tests for model toggle revert, text-field revert, optional/default fields, and meaningful model reorder.

Activity

  1. added
    bugSomething isn't working
    needs-triageMaintainer needs to evaluate this issue
    ready-for-agentFully specified, ready for an AFK agent
    and removed
    needs-triageMaintainer needs to evaluate this issue
    on Jul 12, 2026
  2. self-assigned this
    on Jul 13, 2026
  3. NOirBRight commented on Jul 13, 2026

    @NOirBRight
    OwnerAuthor

    Dispatch

    • Profile: mechanical-worker qualification
    • Model: Ollama Cloud GLM-5.2 via CodexHub Responses; no Luna/Terra fallback
    • Base: origin/dev@29161909c3e650e2e06d72d35a043cb21a04ab51
    • Branch: codex/issue-84-semantic-provider-dirty
    • Ownership: canonical semantic Provider comparison boundary, Provider edit/revert behavior, navigation-guard integration, focused frontend/UI-contract regressions
    • Hotset: frontend/src/pages/ProvidersPage.tsx; relevant frontend/src/lib provider normalization/comparison helper; frontend/scripts/ui-contract.test.mjs; narrowly related frontend tests
    • Non-goals: Provider persistence schema, model priority semantics, Gateway/routing/protocol behavior, unrelated settings UI
    • Verification: red-before/green-after regressions for toggle/text/default-field/reorder cases; frontend build and UI contract; report-only gate; diff hygiene; GLM route identity and normal shell/apply_patch/shell tool use; two-axis review
    • PR target: dev

    This is the first real sidebar-visible GLM mechanical/standard Worker qualification after #105/#108 merged. Preserve meaningful model ordering, fail closed on ambiguous normalization, use the normal apply_patch tool contract, and deliver material WORKER_SIGNAL callbacks to the Orchestrator.

  4. NOirBRight commented on Jul 14, 2026

    @NOirBRight
    OwnerAuthor

    Implementation plan

    Root cause: ProviderDetail computes dirty state via JSON.stringify(draft) !== JSON.stringify(normalizedProvider). The baseline is normalized by normalizeProviderEndpointSelection (endpoint defaults only) but model edits run through normalizeModel, which materializes omitted defaults (context_window: null, input_modalities: ["text"], supported_reasoning_levels: [], default_reasoning_level: null). Reverting a visible toggle leaves these hidden keys in the draft, so JSON.stringify never matches the baseline.

    Plan:

    1. Add one canonical provider comparison helper under frontend/src/lib/providerComparison.ts that:

      • Normalizes both baseline and draft through the same boundary: endpoint selection + nested models.
      • Treats omitted fields and their persisted default equivalents as equal (e.g., missing context_window equals null).
      • Preserves meaningful models[] ordering - a reorder is a real change.
      • Produces a stable serialized form for comparison, not raw JSON.stringify of unequal-shape objects.
    2. Replace the JSON.stringify dirty computation in ProviderEditor.tsx with the canonical comparison.

    3. Update ui-contract.test.mjs to assert the new comparison boundary instead of the raw JSON.stringify pattern.

    4. Add red-before/green-after regression tests for:

      • Toggle revert (enabled toggle changed and restored to not dirty)
      • Text-field revert (display_name changed and restored to not dirty)
      • Optional/default fields (omitted vs null vs empty array to not dirty)
      • Meaningful model reorder (order swapped to dirty)

    Hotset: frontend/src/pages/ProvidersPage.tsx, frontend/src/components/providers/ProviderEditor.tsx, frontend/src/lib/providerComparison.ts, frontend/scripts/ui-contract.test.mjs

    Non-goals: Provider persistence schema, model priority semantics, Gateway/routing/protocol behavior, unrelated settings UI.

  5. added a commit that references this issue on Jul 14, 2026
  6. NOirBRight commented on Jul 14, 2026

    @NOirBRight
    OwnerAuthor

    Completed by PR #123 and merged into dev at 0360aed. The Orchestrator delta review passed; provider comparison regressions are 10/10 and all six CI checks passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions