Skip to content

Configurable macro#201

Closed
cooper (czxtm) wants to merge 31 commits into
developfrom
feat/configurable-macro
Closed

Configurable macro#201
cooper (czxtm) wants to merge 31 commits into
developfrom
feat/configurable-macro

Conversation

@czxtm
Copy link
Copy Markdown
Member

@czxtm cooper (czxtm) commented May 25, 2026

Summary

image

This PR implements the ability to make any parameter configurable via the settings UI by simply adding derive macro (Configurable ). The above is achieved with just a few lines added:

#[derive(Configurable, Debug, Clone)]
#[config(store_path = "settings.json")]
pub struct EvolutionLimits {
    #[config(default = 25, key = "maxIterations")]
    pub max_iterations: usize,

    #[config(default = 5, key = "maxBuildAttempts")]
    pub max_build_attempts: usize,
}

It also adds an import/export button that will contain these configurable values. This enables two important features:

  1. We can share settings and include them in bug reports
  2. If this file is placed inside of the user's nixmac repo at .nixmac/settings.json, then a user can sync their nixmac settings between machines as well!

Test Plan

  • cargo test --manifest-path apps/native/src-tauri/Cargo.toml settings_io
  • cargo test --manifest-path apps/native/src-tauri/Cargo.toml dev_configs
  • cargo test --manifest-path apps/native/src-tauri/Cargo.toml configurable_scope
  • cargo test --manifest-path apps/native/src-tauri/Cargo.toml default_matches_configured_field_defaults
  • bunx vitest run --project=storybook src/components/widget/settings/auto-config-field.stories.tsx src/components/widget/settings/auto-tuning-section.stories.tsx
  • Open Storybook and inspect Settings/AutoConfigField and Settings/AutoTuningSection.

Docs

  • Docs updated (companion PR in darkmatter/nixmac-web: #___)
  • No docs update needed

cooper (czxtm) and others added 16 commits May 24, 2026 01:10
Both imports were only referenced inside JSX comments, so tsc flagged
them (TS6133/TS6192) and broke the build check on main since bd658af.

The env.ts BooleanFromString errors mentioned in the issue do not
reproduce: apps/native resolves effect to 3.21.0 (transitive) which
still exports BooleanFromString. Only the root workspace pulls
effect@4.0.0-beta.62, but env.ts lives under apps/native.

Closes nixmac-62s.
The repo's bun.lock pins effect@4.0.0-beta.62 (no transitive effect@3.x).
4.0 removed Schema.BooleanFromString — which env.ts was using — so a
fresh `bun install --frozen-lockfile` (what CI does) produces a tree
where the import fails to compile. The earlier "doesn't reproduce
locally" diagnosis on nixmac-62s was misled by a stale symlink at
apps/native/node_modules/effect pointing into node_modules/.bun/effect@3.21.0/
left over from an older install run. Locally tsc resolved against the
stale 3.x version, where BooleanFromString still exists.

Migration:
- Replace `Schema.BooleanFromString` with
  `Schema.Literals(["true", "false"])` (4.0's plural array form; 3.x had
  variadic `Schema.Literal(...)` — these are not interchangeable).
- Coerce the validated string to `boolean | undefined` in code so the
  exported settings type matches the old BooleanFromString shape and
  downstream consumers (utils.ts:19's `!== true` check) don't need to
  change.
- Drop the auto-derived `Schema.Schema.Type<typeof Settings>` because
  the runtime shape we expose (with the string→bool coercion) differs
  from the decoded shape.

Strict parsing is preserved: any value other than "true"/"false" still
fails at decode time, same as the original.

Note: existing local installs with a stale apps/native/node_modules/effect
symlink will need `bun install` to refresh — the stale 3.x effect doesn't
have `Schema.Literals` (plural). One-time cost; matches CI from then on.

Refs nixmac-62s.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Previous commit 0895711 accidentally included unresolved 'Updated upstream
/ Stashed changes' markers from a botched git stash pop. Re-exporting via
bd export produces the correct merged state including nixmac-k8d as the
fourth nixmac-srn blocker.
* feat(ci): nightly develop→main release with minor version bump

Adds a GitHub Actions cron (09:00 UTC daily) that merges develop into
main and tags a vMAJ.(MIN+1).0 release. Exits silently when develop has
no new commits vs main.

The actual ship work is delegated to build.yaml's existing `tag` mode —
the nightly job just produces the merge commit and the tag, then atomically
pushes both. compute-version.sh gets a small defensive edit: when HEAD is
already tagged with a v* tag, `release` mode is demoted to `branch` so the
main-push event doesn't trigger a duplicate (patch-bumped) ship alongside
the tag-push event.

Prerequisites for the cron to push successfully:
- RELEASE_BOT_TOKEN secret (PAT with bypass), OR
- repo ruleset bypass for github-actions[bot] on main

Without either, `git push --atomic` fails on the protected main branch.
Workflow falls back to GITHUB_TOKEN so cron + script wiring can be tested
before bypass is granted.

Files:
- .github/workflows/nightly-release.yaml (new)
- ops/scripts/release/nightly-release.sh (new; supports DRY_RUN=1)
- ops/scripts/release/compute-version.sh (tag-aware release-mode skip)

* feat(ci): scope nightly release trigger to native build affecteds

Replace the simple "any commit on develop" trigger with the no-turbo
equivalent of `turbo run build --affected --filter=native`. Releases now
fire only when develop has changes inside the native workspace, its
transitive workspace deps (currently @nixmac/ui), or global build inputs
(root package.json, bun.lock, root tsconfig, Cargo.toml/lock).

This skips nights where develop only got CI/docs/release-script changes
so a minor version isn't burned on commits that wouldn't ship anything
different to users.

Adds ops/scripts/release/affected-paths.mjs which dynamically resolves
the affected path set by reading the root workspaces config and walking
workspace:* deps — no hardcoded list to maintain when new packages
appear.

Verified locally:
- Real diff (apps/native/*, Cargo.lock, etc.): release
- Synthetic diff (ops/, .github/, docs/, *.md): skip
- Synthetic diff (bun.lock only): release (global input)

* fix(ci): address Copilot review feedback on nightly-release

- next_minor_version: filter to stable vMAJ.MIN.PATCH only so disposable
  -test.N tags (used by build.yaml for signing rehearsals) can't pollute
  the bump base
- Remote tag existence check: use `git ls-remote --exit-code` with an
  exact ref pattern instead of `grep -q "${tag}"`, which would false-
  positive when a longer tag like v1.2.0-test.1 contains the candidate
  as a substring
- Docstring: align step 2 with actual behavior (logs a one-liner then
  exits 0, not strictly silent)
- Workflow: add concurrency group so a scheduled run + manual dispatch
  can't race on the same next-tag computation. cancel-in-progress=false
  to avoid aborting a half-completed merge+tag mid-flight

* docs(ci): align skip-message wording with affected-paths policy

Copilot follow-up: three places still described the skip condition as
"no new commits vs main" or "skip silently". The actual policy is the
affected-paths filter (no-turbo equivalent of
`turbo run build --affected --filter=native`), which can return false
even when develop has commits — they just don't touch the native build
graph.

- should_release docstring: "skip silently" → "skip (caller logs reason)"
- main() skip log: "develop has no new work" → "no changes affect the
  native build graph"
- workflow header: "no new commits vs main" → explanation of the
  affected-paths filter and what kinds of develop activity will no-op

No behavior change.

* fix(ci): nightly-release cwd-independence and ..→... range

Copilot follow-up. Two real bugs:

1. The script claimed cwd-independence via REPO_ROOT but only used it for
   the affected-paths.mjs invocation. Every git command and the
   `node -p require('./package.json')` fallback ran relative to the
   caller's cwd, so invoking the script from anywhere other than the
   repo root would fail. main() now `cd`s into REPO_ROOT before doing
   any work, which makes the cwd-independence claim actually true.

2. should_release used `git diff origin/main..origin/develop` (two-dot
   range), which is tip-to-tip. If main has a hotfix not on develop,
   two-dot treats it as a develop-side deletion and flags it as a
   change — falsely triggering a release every night until the hotfix
   gets merged back. Three-dot (`origin/main...origin/develop`) diffs
   from the merge-base, so only commits new on develop are counted.

Verified locally: running the script from /tmp now produces the same
dry-run plan as running from the repo root. Two-dot vs three-dot on
current origin/main vs origin/develop differs by one file, confirming
there is currently a hotfix-style commit on main that the old check
would have erroneously counted.

* docs/fix(ci): nightly-release token fallback + optional-env docstring

Copilot follow-up:

1. Workflow checkout's token fallback used `secrets.GITHUB_TOKEN` as the
   `||` second operand. While that *does* work in normal expression
   contexts (it's GitHub's documented way to reference the auto-injected
   token), the `github.token` context form is more reliable in fallback
   positions and `if:` evaluations. Switched to
   `secrets.RELEASE_BOT_TOKEN || github.token` for that defensive reason.

2. Header docstring claimed GIT_USER_NAME / GIT_USER_EMAIL were "Required
   env" but the code uses `if [[ -n "${VAR:-}" ]]` guards and silently
   skips git config when unset. They're actually optional CI overrides
   that fall back to the caller's existing git config — fine for local
   rehearsals, expected to be set in CI for bot-identity attribution.
   Updated the comment to match the actual code.

No behavior change.

* fix(ci): defensive Copilot follow-ups on nightly-release

1. DRY_RUN expression now gates on `github.event_name == 'workflow_dispatch'`
   before reading `inputs.dry_run`. The `inputs.*` context is only
   populated for dispatch events; gating ensures scheduled cron runs
   always get '0' regardless of how GitHub resolves missing-input refs.

2. compute-version.sh tag-skip regex now anchored with `$` so disposable
   `v0.22.0-test.N` tags (used for signing/notarization rehearsals)
   don't suppress legitimate `main`-push releases. Test tags don't ship
   (publish/R2/Linear steps in build.yaml skip them), so main-push must
   still bump+ship normally if such a tag happens to be at HEAD.
   Verified: `v1.2.3` matches the regex (correctly skips release mode),
   `v1.2.3-test.1` does not match (correctly does not suppress).

3. affected-paths.mjs now normalizes `package.json#workspaces` to handle
   both the npm/bun array form and the Yarn object form
   (`{ packages: [...] }`). nixmac uses the array form today; supporting
   both is cheap future-proofing if the repo ever switches package
   manager.

* docs/fix(ci): nightly-release docstring + unconditional fetch

Copilot follow-up:

1. Header step 4 said "Fast-forward / no-ff merge" but the implementation
   always uses `git merge --no-ff`. Updated the comment to match the
   actual behavior (always produces a merge commit so the release
   boundary stays visible in git log --first-parent).

2. `git fetch` was DRY_RUN-gated, but should_release and
   next_minor_version both query local refs (origin/main..origin/develop
   diff + `git tag --list` for the latest stable). On a stale checkout
   the dry-run could compute a wrong next version or false-positive
   "nothing to release". Unwrap the fetch from `run` so it always
   executes — fetch is read-only from the project's perspective (only
   updates local origin/* refs), so unconditional execution is safe and
   makes dry-run output reflect real remote state.

Verified: the unconditional fetch pulled a fresh v0.23.2 tag on this
run that the local checkout didn't have, validating the fix on its
first execution.

* fix(native): fix Chromatic Storybook

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(native): env.ts — drop version-skewed Schema.Literal for plain String

PR #195 build broke at TS2554 ("Expected 1 arguments, but got 2") after
80a19c6 reverted NIX_INSTALLED_OVERRIDE from `Schema.Literals([...])` to
`Schema.Literal(...)` (variadic). The problem: `Schema.Literal`'s signature
is fundamentally version-skewed —

  effect 3.x:       `Literal<L>(...values: L)`     variadic, multi-value
  effect 4.0-beta:  `Literal<L>(value: L)`         single value only,
                    use `Literals([...])` for sets

So neither form source-compiles in both:

- `Schema.Literal("true", "false")` works in 3.x, fails 4.0 (this PR)
- `Schema.Literals(["true", "false"])` works in 4.0, fails 3.x (Chromatic)

The repo's lockfile pins effect 4.0-beta, so tsc sees that — but the
Chromatic/storybook test environment can resolve 3.x via workspace
symlink hoisting, which is why 80a19c6 picked the 3.x form to fix
Chromatic and broke the build instead.

Fix: validate with plain `Schema.String` (signature is identical across
versions) and coerce to boolean in code via
`raw.NIX_INSTALLED_OVERRIDE === "true" ? true : undefined`. The only
downstream consumer is `settings.NIX_INSTALLED_OVERRIDE !== true` in
widget/utils.ts, which treats any non-"true" value as functionally
false — so silent acceptance of unexpected strings is observationally
equivalent to coercing them to undefined. We lose schema-level
"must be 'true' or 'false'" strictness; we gain working CI on both
the build and Chromatic paths.

Verified against both effect@3.21.0 and effect@4.0.0-beta.62 symlinks
in isolation: no TS2554, no missing-Literals errors.

* fix(ci): nightly-release DRY_RUN final-message clarity

Copilot follow-up: the final `echo "Released ${tag}"` ran unconditionally,
so workflow_dispatch dry-runs printed "Released v0.24.0" even though the
merge/tag/push steps were only echoed. Now branches on DRY_RUN: prints
"Dry run complete — would have released ${tag}" in dry mode, "Released
${tag}" in real mode. Behavior preserved; just the operator-facing log
line distinguishes the two modes.

Did not touch the `local changed paths file path` declaration in
should_release — Copilots claim that `path` is unused is incorrect.
`path` is the inner loops read variable on line 85 and is used in the
path-matching comparisons on lines 87, 89, 91, and 92.

* fix(ci): nightly-release working-tree guard + setup-node pin

Copilot follow-up:

1. Add a clean-working-tree guard before the destructive `git reset --hard
   origin/${MAIN_BRANCH}`. Local accidental invocations would otherwise
   silently obliterate uncommitted work. CI runners start clean by
   construction so the guard never fires in CI; DRY_RUN=1 bypasses it
   since dry-mode does not mutate the tree. Error path prints
   `git status --short` so the operator can see what would have been lost.

2. Add an actions/setup-node@v6 step pinned to node-version: 20 (matching
   build.yaml's convention). The release script invokes node for
   affected-paths.mjs and the package.json version fallback in
   next_minor_version. Without an explicit pin the cron would depend on
   whatever node the ubuntu-latest runner image ships, which is not stable
   across image updates.

Verified: bash -n passes, dry-run still works, guard simulation triggers
correctly on dirty tree and stays out of the way in DRY_RUN=1.

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… (PoC)

Introduces a proc-macro that eliminates the 6-file boilerplate currently
required for every store-backed setting. A new knob today touches:
storage/store.rs (getter+setter), shared_types/prefs.rs (UiPrefs +
UiPrefsUpdate fields), commands/ui_prefs.rs (read+write branches), TS
bindings, and UI forms — ~25 LOC across 6 files. With this derive, a new
knob is one struct field.

PoC scope — EvolutionLimits only:

  #[derive(Configurable)]
  #[config(store_path = "settings.json")]
  pub struct EvolutionLimits {
      #[config(default = 25, key = "maxIterations")]
      pub max_iterations: usize,
      #[config(default = 5, key = "maxBuildAttempts")]
      pub max_build_attempts: usize,
  }

The derive generates `EvolutionLimits::load(app) -> Result<Self>` that
reads each field from tauri-plugin-store with the per-field default
fallback. Reads happen on every call, so edits via dev settings take
effect on next agent run (hot-reload).

Two new workspace crates:
- configurable/         — runtime trait + read_field() helper
- configurable-derive/  — proc-macro

Bug fix included: evolve/mod.rs:1467 was logging
DEFAULT_MAX_BUILD_ATTEMPTS (the const) instead of the configured value
— exactly the drift this derive is designed to prevent. Threaded
max_build_attempts into process_tool_result().

UiPrefs / UiPrefsUpdate / ui_set_prefs left untouched on purpose — full
migration is tracked under nixmac-e53 with one sub-issue per category.

Closes: nixmac-8ka

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 25, 2026 02:55
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

Warnings
⚠️ Please assign this PR to someone (usually yourself).
⚠️ ❗ Big PR (3909 lines changed). Consider splitting it into smaller, focused changes.
Messages
📖 No docs update needed — acknowledged.

📋 PR Overview

Lines changed 3909 (+3621 / -288)
Files 16 added, 22 modified, 0 deleted
Draft / WIP no
Has Test Plan yes
New UI components yes (2)
New Storybook stories yes (2)
New Rust modules yes (5)
New TS source files yes (2)
New tests no
package.json touched no
Cargo.toml touched yes
Infra / CI touched no

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 17.8% 17.8% 31.2% 54.0%

Generated by 🚫 dangerJS against 0c4a504

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a proof-of-concept Rust #[derive(Configurable)] proc-macro (plus small runtime helper crate) to simplify reading hot-reloadable developer settings from tauri-plugin-store, and migrates the evolution loop’s iteration/build-attempt limits to use it.

Changes:

  • Introduces two new workspace crates: configurable (runtime helper + derive re-export) and configurable-derive (proc-macro generating Struct::load(app)).
  • Migrates evolution limits (max_iterations, max_build_attempts) to a new EvolutionLimits struct loaded from settings.json, and threads the configured max_build_attempts into build-check logging/control flow.
  • Updates workspace/native Cargo manifests and lockfile to include the new crates; closes the related beads issue.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Cargo.toml Adds new workspace members for the macro + runtime crates.
Cargo.lock Records the new crates and their dependencies in the lockfile.
apps/native/src-tauri/src/evolve/mod.rs Loads EvolutionLimits via the new derive and threads max_build_attempts into processing/logging.
apps/native/src-tauri/src/evolve/config.rs Defines EvolutionLimits annotated with #[derive(Configurable)] and store keys/defaults.
apps/native/src-tauri/configurable/src/lib.rs Adds runtime helper read_field() and re-exports the derive.
apps/native/src-tauri/configurable/Cargo.toml Declares the new runtime crate and dependencies.
apps/native/src-tauri/configurable-derive/src/lib.rs Implements the Configurable derive that generates a load() method.
apps/native/src-tauri/configurable-derive/Cargo.toml Declares the new proc-macro crate and dependencies.
apps/native/src-tauri/Cargo.toml Adds the new configurable path dependency to the native Tauri crate.
.beads/issues.jsonl Marks nixmac-8ka as closed and adds follow-up migration issues.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/native/src-tauri/src/evolve/mod.rs Outdated
Comment thread apps/native/src-tauri/configurable-derive/src/lib.rs Outdated
Comment on lines +1 to +7
//! Configurable — store-backed dev settings without per-knob boilerplate.
//!
//! Derive `Configurable` on a struct to generate a `load(app)` method that reads
//! each field from `tauri-plugin-store`, falling back to the per-field default.
//! Edits made by the user via the dev-settings UI (which writes to the same
//! store keys) are picked up on the next `load(app)` call — i.e. hot-reload.
//!
@czxtm cooper (czxtm) changed the title Feat/configurable macro Configurable macro May 25, 2026
cooper (czxtm) and others added 4 commits May 24, 2026 20:17
The Tuning section is a new home for behavior knobs that grow over time as
more Configurable structs are migrated. Starts with the same two settings
(max iterations, max build attempts) that were previously hidden inside
the AI Models tab — moved here because they're not about models.

The Backup & Restore section adds:
  - Export settings.json — opens a save dialog and writes a filtered copy
    of the plugin-store. By default legacy plain-text API keys
    (openrouterApiKey, openaiApiKey, vllmApiKey) are stripped. A checkbox
    opts into including them for full backups.
  - Import settings.json — opens an open dialog, validates the file as a
    JSON object, then REPLACES the entire store with the file contents.
    A confirmation dialog warns that absent keys (including API keys, if
    the export was sanitized) will be cleared.

Both sections are gated by developerMode, inherited from the Developer
tab itself.

Backend:
- commands/settings_io.rs with settings_export(include_secrets: bool)
  and settings_import() Tauri commands, both gated on developer mode
- shared_types/settings_io.rs with ExportResult / ImportResult (specta
  generates the matching TS types automatically)

Frontend:
- ai-models-tab.tsx: Evolution Limits subsection removed; max iterations
  and max build attempts props dropped
- settings-dialog.tsx: matching cleanup of unused form fields and the
  form.Field wrappers that fed AiModelsTab
- developer-tab.tsx: two new sections; loads tuning values via direct
  tauriAPI.ui.getPrefs() rather than the form, keeping the storybook
  story self-contained
- Snapshots updated for ai-models-tab and developer-tab

Closes: nixmac-cqb

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…settings.json)

EvolutionLimits (max iterations, max build attempts) now persists in
<config_dir>/.nixmac/settings.json instead of the OS app data directory.
Because the config dir is the user's nix-darwin repo and already syncs
via git, these settings ride along across machines without an explicit
backup/restore step.

Per-device settings (developerMode, pinnedVersion, updateChannel, model
cache, prompt history) stay in the OS app data dir where they belong.

How it works:

- New storage::configurable_scope module exposes repo_store_path(app),
  which returns the absolute path to .nixmac/settings.json and lazily
  creates the directory plus a short README.md the first time it's
  called.

- configurable-derive learns #[config(store_path_fn = <path>)]: instead
  of a hard-coded string, the generated load() method calls a resolver
  function each time. Keeps the derive decoupled from nixmac — any
  consumer can plug in their own path resolver.

- EvolutionLimits opts into the new mode via store_path_fn.

- store::get_max_iterations / set_max_iterations and the
  max_build_attempts pair now read/write the repo store, with a
  best-effort migration from the legacy local store on first read so
  existing users don't see their values reset to defaults. Falls back
  to defaults if config_dir isn't set yet (during onboarding).

- Tuning UI section copy clarifies that values sync via the repo. The
  Backup & Restore section copy clarifies that repo-scoped values are
  versioned by git, not included in the local export.

Also includes the user's separate rename of "vLLM / LiteLLM" → "OpenAI
Compatible" in ai-models-tab.tsx and api-keys-tab.tsx.

Closes: nixmac-gr3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 25, 2026 03:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.

Comment thread apps/native/src-tauri/configurable-derive/src/lib.rs Outdated
Comment on lines +61 to +65
const next = Number.parseInt(raw, 10);
if (Number.isNaN(next)) return;
setMaxIterations(next);
try {
await tauriAPI.ui.setPrefs({ maxIterations: next });
Comment on lines +72 to +76
const next = Number.parseInt(raw, 10);
if (Number.isNaN(next)) return;
setMaxBuildAttempts(next);
try {
await tauriAPI.ui.setPrefs({ maxBuildAttempts: next });
Comment thread apps/native/src-tauri/src/commands/settings_io.rs Outdated
Comment thread apps/native/src-tauri/src/commands/settings_io.rs Outdated
Comment thread apps/native/src-tauri/src/evolve/mod.rs Outdated
Comment on lines +458 to +461
config::EvolutionLimits {
max_iterations: 25,
max_build_attempts: 5,
}
Comment on lines +549 to +553
let value = get_repo_store(app)
.ok()
.and_then(|s| s.get("maxBuildAttempts"))
.and_then(|v| serde_json::from_value::<usize>(v).ok())
.unwrap_or(5);
cooper (czxtm) and others added 7 commits May 24, 2026 21:45
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…ry registry

Adding a new field to a Configurable struct now automatically appears in
the Tuning section of the Developer settings tab — no frontend changes
required. Schema metadata (label, type, range, defaults, help text) is
emitted by the derive and consumed by a generic <AutoConfigField> React
component.

How it works:

1. The configurable runtime crate gains schema types (ConfigurableSchema,
   ConfigField, FieldType, EnumVariant) that flow to TS via specta. A
   type-erased RegisteredConfig holds fn pointers per struct; the derive
   submits one to a global inventory at compile time.

2. The derive parses an expanded attribute grammar:
     #[config(
         store_path_fn = ...,
         display_name = "Evolution",
         description = "...",
     )]
     pub struct EvolutionLimits {
         #[config(
             default = 25,
             key = "maxIterations",
             label = "Max iterations",
             range = 1..=200,
             help = "API calls before stopping",
         )]
         pub max_iterations: usize,
     }

   FieldType is inferred from the Rust type: numeric primitives become
   Number{min,max}, bool becomes Boolean, String becomes String{multiline}.
   Any field annotated with #[config(options = ["a", "b"])] becomes an
   Enum regardless of its Rust type — labels are humanized from the
   variant values.

3. Each struct's derive generates:
     - load<R>(app)              — read all fields with defaults
     - schema<R>(app)            — full schema + current values
     - set_field<R>(app, k, v)   — type-checked single-field write
     - Wry-monomorphic shims for the fn pointers in RegisteredConfig

4. Two Tauri commands wrap the registry:
     - dev_configs_list          — walks inventory, returns Vec<Schema>
     - dev_config_set            — dispatches by struct name to set_field

5. <AutoConfigField> renders the right control per FieldType.kind:
   number → <Input type="number" min/max>, boolean → <Switch>,
   string → <Input> (or <Textarea> when multiline), enum → <Select>.
   Help text becomes a tooltip on an info icon.

6. <AutoTuningSection> calls dev_configs_list once, renders a section
   per registered struct, and writes via dev_config_set. Replaces the
   ~85 lines of hand-written Tuning JSX in developer-tab.tsx with
   <AutoTuningSection />.

Type-checking on set: the generated set_field round-trips the incoming
JSON value through the declared Rust type before writing. An out-of-range
or wrong-type value surfaces as an Error in the UI inline beneath the
field rather than corrupting the store.

Storybook compatibility: AutoTuningSection treats a null response from
invoke as "no registered configs" (Tauri commands aren't real in
storybook), so the snapshot still renders cleanly.

Closes: nixmac-93p

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@czxtm cooper (czxtm) enabled auto-merge May 25, 2026 06:22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The overall idea of having ways to easily declare customization points that show up in the UI is cool, nice stuff.

First, this is a massive PR that it's a bit tricky to review. I would have loved to see it split in three parts:

  1. The Configurable and auto-UI infrastructure.
  2. The saving of settings.json in the config folder.
  3. The new theme.

Going into the code itself, I see a big missed opportunity to start moving in the direction I suggest in my architecture review document. This would set the groundwork to eventually move all the preferences into Configurable infrastructure. This would imply:

  1. Storing the configurable in tauri::State instead of store.
  2. By doing that, we already remove a lot of code, since there is no need to marshall the configurable into/outo the store, since Serde can do that for us (and will give us versioning, etc.)
  3. Adding an update channel to update the UI (via a regular API based on events, as described in the architecture review)
  4. Using the an auto-updated zustand store (or store slice) as way to transparently interface between the UI and Rust.

The details on how to do this are already broadly in the architecture review document. If you don't have time to do this, perhaps Cas Linden (@CasLinden) can pick this up, as I know he wanted to get familiarity with the architecture and start implementing it in various parts of the application.

Comment on lines +510 to +524
/// Copies a key from the legacy local store into the repo store the first
/// time a repo-scoped getter runs (post-upgrade migration). No-op once the
/// key already exists in the repo store.
fn migrate_repo_key<R: Runtime>(app: &AppHandle<R>, key: &str) -> Result<()> {
let repo = get_repo_store(app)?;
if repo.get(key).is_some() {
return Ok(());
}
let local = get_store(app)?;
if let Some(value) = local.get(key) {
repo.set(key, value);
repo.save()?;
}
Ok(())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure we should worry about "legacy" at this level since we haven't released yet, there's some risk of just acummulating lots of unneeded code. Eventually, we should think about versioning of various files (including settings.json) but this should be done using the versioning infrastructure of the serialization infrastructure (e.g. Serde.) Related to the general comment on moving away from tauri::store.

Comment on lines +29 to +31
useEffect(() => {
refresh();
}, [refresh]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

refresh is not used anywhere else if I'm understanding correctly, so the body could be brought in here


const commit = async (next: unknown) => {
const previous = value;
setValue(next);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems to be the only place where we update the UI, there's no event driven to update the UI from changes from RS. This is risky.

@czxtm
Copy link
Copy Markdown
Member Author

cooper (czxtm) commented May 28, 2026

Juanpe Bolívar (@arximboldi) agreed this PR got too big after I decided the channel between rust/js should be more strict - I'll split them up.

Regarding tauri::Store though, I'm not sure how that would work given that we want to store this as JSON so these settings can persist across devices? tauri::Store is a type-keyed map (TypeId → value) that lives in RAM for the process lifetime, not ideal for things we want to persist to disk, A Store is a HashMap<String, JsonValue> backed by a file on disk. Or did you mean for it to be used as a way to transmit data between the js/rust boundary?

@czxtm cooper (czxtm) marked this pull request as draft May 28, 2026 01:27
auto-merge was automatically disabled May 28, 2026 01:27

Pull request was converted to draft

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants