Skip to content

feat: add image subfolder strategy setting UI#9133

Merged
lstein merged 2 commits intoinvoke-ai:mainfrom
JPPhoto:subfolder-strategy-ui
May 8, 2026
Merged

feat: add image subfolder strategy setting UI#9133
lstein merged 2 commits intoinvoke-ai:mainfrom
JPPhoto:subfolder-strategy-ui

Conversation

@JPPhoto
Copy link
Copy Markdown
Collaborator

@JPPhoto JPPhoto commented May 7, 2026

Summary

Added the configurable image subfolder strategy to the Settings panel. Updated documentation for the YAML config file. Also make changing the strategy act in realtime to future saves.

Admins can now set image_subfolder_strategy from the UI. The setting is persisted through the existing runtime config endpoint to invokeai.yaml. The API schema, frontend generated types, English locale strings, tests, and YAML config docs were updated.

Related Issues / Discussions

Related to PR #8969.

QA Instructions

Verify manually as an admin or when in single-user mode:

  1. Open Settings.
  2. Change Image Subfolder Strategy under Generation.
  3. Confirm invokeai.yaml is updated.
  4. Confirm that saving a new image immediately uses the correct subfolder structure.

Confirm non-admin multiuser accounts cannot update the setting.

Merge Plan

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

@JPPhoto JPPhoto added the v6.13.x label May 7, 2026
@github-actions github-actions Bot added api python PRs that change python files frontend PRs that change frontend files python-tests PRs that change python tests docs PRs that change docs labels May 7, 2026
@JPPhoto JPPhoto moved this to 6.13.x Theme: MODELS in Invoke - Community Roadmap May 7, 2026
@Pfannkuchensack
Copy link
Copy Markdown
Collaborator

Findings

  • Medium: invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsImageSubfolderStrategySelect.tsx:18-23 hardcodes the four strategy options (flat, date, type, hash) rather than deriving them from the backend IMAGE_SUBFOLDER_STRATEGY literal exposed via schema.ts. The satisfies ImageSubfolderStrategyOption[] annotation only enforces that each declared value is assignable to ImageSubfolderStrategy (defined at line 11 as NonNullable<S['InvokeAIAppConfig']['image_subfolder_strategy']>), not that the union is exhaustively covered. The new test invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsImageSubfolderStrategySelect.test.ts:11 hardcodes toEqual(['flat', 'date', 'type', 'hash']), so it exercises the constant against itself and cannot detect drift. If a backend developer adds a fifth literal in IMAGE_SUBFOLDER_STRATEGY at invokeai/app/services/config/config_default.py:33 and regenerates the OpenAPI schema, the dropdown silently omits the new value and admins cannot select it from the UI even though the PATCH endpoint accepts it.
    To expose this issue, add a vitest that asserts every literal of the S['UpdateAppGenerationSettingsRequest']['image_subfolder_strategy'] union appears in imageSubfolderStrategyOptions (e.g., via an exhaustiveness pattern using Exclude<Strategy, OptionValue> resolving to never).

  • Low: invokeai/app/api/routers/app_info.py:112-116 declares image_subfolder_strategy: IMAGE_SUBFOLDER_STRATEGY = Field(default=None, ...). The annotation IMAGE_SUBFOLDER_STRATEGY = Literal["flat", "date", "type", "hash"] does not include None, so the declared default is not a valid value for the type. The json_schema_extra=lambda schema: schema.pop("default", None) hides this in the generated OpenAPI (and schema.ts shows the field as a clean four-value enum), and model_dump(exclude_unset=True) plus runtime literal validation make the PATCH endpoint behave correctly today (verified by test_update_runtime_config_rejects_null_image_subfolder_strategy). However the pattern is fragile: a future Pydantic release that validates default values, or a developer copy-pasting the same idiom for a non-Literal-only field, can either start raising at import time or accidentally widen the request type to allow null. A safer construction is image_subfolder_strategy: IMAGE_SUBFOLDER_STRATEGY | None = Field(default=None, ...) paired with an explicit model_validator that rejects None, or simply omitting the strategy field from model_fields_set via the existing exclude_unset flow without lying about the type.
    To expose this issue, add a backend test that loads app.openapi() and asserts the schema for UpdateAppGenerationSettingsRequest.image_subfolder_strategy contains exactly the four enum values, no default, and no null or None permitted, so any regression in the JSON Schema lambda is caught.

  • Low: invokeai/app/api/routers/app_info.py:138-153 performs a read-modify-write of invokeai.yaml (load_and_migrate_config -> update_config -> write_file) without acquiring any lock. The external-provider write path at invokeai/app/api/routers/app_info.py:248-286 holds _EXTERNAL_PROVIDER_CONFIG_LOCK while it does the equivalent. A concurrent PATCH /runtime_config and POST /external_providers/config/{provider_id} (or two concurrent PATCHes from an admin clicking quickly) can interleave so that the later writer clobbers the earlier writer's persisted change. The race is pre-existing for max_queue_history, but this branch enlarges the writable surface and increases the chance of admin-initiated concurrent edits in the same Settings panel.
    To expose this issue, add a backend test that injects a small delay between the load and write inside update_runtime_config (via monkeypatch of load_and_migrate_config), fires a concurrent POST /external_providers/config/openai from a thread, and asserts both updates are still on disk afterwards.

  • Low: tests/app/routers/test_app_info.py:181-194 only asserts the response body and the on-disk yaml. It does not assert that the runtime config the production image-creation path reads (self.__invoker.services.configuration.image_subfolder_strategy at invokeai/app/services/images/images_default.py:60) reflects the new value. In production the InvocationServices.configuration instance is the same get_config() singleton injected at invokeai/app/api/dependencies.py:123,198, so the wiring works, but the test fixture in tests/conftest.py:35 constructs a separate InvokeAIAppConfig(...) and the lru-cached singleton is a different object, so the test as written cannot prove the "newly-created images use the new strategy" guarantee that the docs added in docs/src/content/docs/configuration/invokeai-yaml.mdx promise.
    To expose this issue, add a backend test that PATCHes image_subfolder_strategy and then asserts the strategy resolved by create_subfolder_strategy(get_config().image_subfolder_strategy) matches what was just written, or alternatively invokes ImageService.create and verifies the persisted image_subfolder reflects the new strategy.

  • Low: invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsImageSubfolderStrategySelect.tsx:36 silently coerces runtimeConfig?.config.image_subfolder_strategy ?? 'flat'. If the runtime ever surfaces a value the frontend does not know about (for example a backend literal added without regenerating schema.ts, or a future migration), the dropdown will display Flat while the actual stored value is something else, and the user has no indication that the displayed selection is a fallback. There is no toast, no warning, and the Combobox value prop becomes undefined (because options.find(...) === undefined) so the control silently shows nothing while internal state believes the strategy is the unknown value.
    To expose this issue, add a vitest that simulates runtimeConfig.config.image_subfolder_strategy === 'unknown' and asserts the component either renders an explicit unknown-value indicator or the option list is augmented with the unknown value rather than collapsing to the default.

@JPPhoto
Copy link
Copy Markdown
Collaborator Author

JPPhoto commented May 7, 2026

@Pfannkuchensack - I implemented the review follow-ups. Fixed:

  • Strategy option exhaustiveness now checks against UpdateAppGenerationSettingsRequest.
  • Nullable schema fragility fixed with explicit | None, validator rejection for explicit null, and OpenAPI schema regression test.
  • Runtime config YAML writes now use the same config lock as external provider writes.
  • Backend test now verifies the runtime singleton reflects the patched strategy.
  • Unknown frontend strategy values now render as Unknown (...) instead of silently collapsing.
  • Added a lock regression test for the runtime config YAML read/write path.

Copy link
Copy Markdown
Collaborator

@Pfannkuchensack Pfannkuchensack left a comment

Choose a reason for hiding this comment

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

Works good.

@JPPhoto JPPhoto force-pushed the subfolder-strategy-ui branch 5 times, most recently from cbbcf4f to 4b6356a Compare May 8, 2026 02:35
@JPPhoto JPPhoto force-pushed the subfolder-strategy-ui branch from 4b6356a to 2c64ec4 Compare May 8, 2026 03:00
Copy link
Copy Markdown
Collaborator

@lstein lstein left a comment

Choose a reason for hiding this comment

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

Works as advertised.

@lstein lstein merged commit aa865f6 into invoke-ai:main May 8, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api docs PRs that change docs frontend PRs that change frontend files python PRs that change python files python-tests PRs that change python tests v6.13.x

Projects

Status: 6.13.x Theme: MODELS

Development

Successfully merging this pull request may close these issues.

3 participants