Skip to content

Add Material pack option#874

Merged
egorikftp merged 7 commits intomainfrom
feature/material-pack
Feb 19, 2026
Merged

Add Material pack option#874
egorikftp merged 7 commits intomainfrom
feature/material-pack

Conversation

@egorikftp
Copy link
Copy Markdown
Member

@egorikftp egorikftp commented Feb 18, 2026

67bc61b9-1627-47d7-a0af-c11b7cb6a985.mov

📝 Changelog

If this PR introduces user-facing changes, please update the relevant Unreleased section in changelogs:

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

A Material Pack flow was added: MaterialPackScreen, MaterialPackViewModel, MaterialPackAction/Event/State, MaterialPackCreation UI and MaterialInputHandler. Directory selection was refactored: new DirectoryState and IconPackDirectoryPicker replace the previous chooser. PackEditState and the ExpandedGroup composable were removed, and the persistent/in-memory useMaterialPack setting was deleted. InputFieldState changed (allowEditing). Multiple UI model types were moved from ui.model to model packages. New IconPackScreenScaffold and several input handlers/viewmodels were introduced or reorganized. DI preview initialization was adjusted or removed in some previews.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Material pack option' directly and clearly describes the main feature addition in the changeset: introducing a Material pack option throughout the plugin UI and configuration.
Description check ✅ Passed The description includes a video asset demonstrating the feature and confirms that all three required changelog sections (IntelliJ Plugin, CLI, Gradle Plugin) have been updated as specified in the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/material-pack

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt (1)

76-90: ⚠️ Potential issue | 🟡 Minor

allowEditing not guarding the add-button in the empty-packs branch.

When nestedPacks.isEmpty(), AddPackButton is rendered unconditionally (lines 78–81), but when nestedPacks.isNotEmpty(), the equivalent button inside NestedPacks is gated behind allowEditing (line 135). The Material pack flow will typically start with an empty nested-pack list, so if it sets allowEditing = false, the "Add nested pack" button would still appear — a functional regression in that flow.

🐛 Proposed fix
-        if (inputFieldState.nestedPacks.isEmpty()) {
-            Spacer(8.dp)
-            AddPackButton(
-                modifier = Modifier.align(Alignment.Start),
-                onClick = onAddNestedPack,
-            )
-        } else {
+        if (inputFieldState.nestedPacks.isNotEmpty()) {
             NestedPacks(
                 nestedPacks = inputFieldState.nestedPacks,
                 allowEditing = inputFieldState.allowEditing,
                 onRemove = onRemoveNestedPack,
                 onValueChange = onValueChange,
                 onAddNestedPack = onAddNestedPack,
             )
+        } else if (inputFieldState.allowEditing) {
+            Spacer(8.dp)
+            AddPackButton(
+                modifier = Modifier.align(Alignment.Start),
+                onClick = onAddNestedPack,
+            )
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt`
around lines 76 - 90, The empty-branch renders AddPackButton unconditionally
which bypasses the allowEditing guard used inside NestedPacks; update the
empty-packs branch in IconPackEditor so AddPackButton is only shown when
inputFieldState.allowEditing is true (e.g., wrap the Spacer + AddPackButton
rendering in a check for inputFieldState.allowEditing or render a
disabled/hidden placeholder when false) so behavior matches the NestedPacks
path; reference the AddPackButton and inputFieldState.allowEditing in your
change.
🧹 Nitpick comments (7)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/inputhandler/InputHandler.kt (1)

30-33: Pre-existing: nestedPacks.size.toString() can produce duplicate IDs.

Scenario: add (id="0"), add (id="1"), remove "0", add → id="1" again. Two packs share "1", and removeNestedPack (which uses filterNot { it.id == nestedPack.id }) would then silently delete both.

The onAddNestedPack lambda in the IconPackEditorPreview already uses Random.nextLong().toString() to avoid this. Consider aligning BasicInputHandler with the same approach or using a UUID.

♻️ Suggested fix
 override fun addNestedPack() {
     _state.updateState {
         copy(
             nestedPacks = nestedPacks + NestedPack(
-                id = nestedPacks.size.toString(),
+                id = java.util.UUID.randomUUID().toString(),
                 inputFieldState = InputState(),
             ),
         )
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/inputhandler/InputHandler.kt`
around lines 30 - 33, The nested pack ID generation in BasicInputHandler
currently uses nestedPacks.size.toString(), which can create duplicate IDs after
removals; change the creation of NestedPack in the onAddNestedPack flow to use a
unique ID generator (match IconPackEditorPreview) such as
Random.nextLong().toString() or UUID.randomUUID().toString() instead of
nestedPacks.size.toString() so each NestedPack.id is globally unique and
removeNestedPack (which filters by id) will only remove the intended pack.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt (1)

51-51: Consider a dedicated string key for the Material Pack save action.

"iconpack.newpack.creation.continue" is semantically scoped to the NewPack flow. Reusing it works today since the button text is identical, but the namespacing will mislead if the two flows ever need different labels.

♻️ Suggested string key
-                Text(text = stringResource("iconpack.newpack.creation.continue"))
+                Text(text = stringResource("iconpack.materialpack.creation.save"))

Add a corresponding entry in the bundle:

iconpack.materialpack.creation.save=Save

(or whatever the final copy is)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt`
at line 51, Replace the reused NewPack string key with a dedicated Material Pack
save key: add a new resource entry (e.g. "iconpack.materialpack.creation.save"
-> "Save" or final copy) to the strings bundle, then update the Text composable
call that currently uses stringResource("iconpack.newpack.creation.continue") to
use stringResource("iconpack.materialpack.creation.save") instead; locate the
usage in MaterialPackCreation (the Text/stringResource call) and ensure the new
key is added to the appropriate localization file.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackViewModel.kt (1)

42-43: Consider using a buffered Channel for event delivery robustness

Channel<NewPackEvent>() defaults to RENDEZVOUS capacity (0), meaning send suspends until a receiver is active. While LaunchedEffect(Unit) in the screen starts collecting quickly, a brief window during recomposition or initial composition could leave the coroutine stuck on _events.send(...) until the collector resumes.

♻️ Proposed change
-    private val _events = Channel<NewPackEvent>()
+    private val _events = Channel<NewPackEvent>(Channel.BUFFERED)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackViewModel.kt`
around lines 42 - 43, The Channel used for event delivery (_events of type
Channel<NewPackEvent> in NewPackViewModel) is rendezvous by default and can
suspend senders during brief gaps in collection; change its construction to a
buffered channel (for example using Channel(Channel.BUFFERED) or a small fixed
capacity like Channel(1)) so _events.send(...) won't suspend if the collector is
not immediately ready, and keep exposing events via events =
_events.receiveAsFlow().
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackDirectoryPicker.kt (1)

42-42: Minor style inconsistency: prefer rememberMutableState over remember { mutableStateOf(...) }

ChooseExistingPackFile.kt (line 48) uses the project's rememberMutableState helper for the same isDragging pattern. Using it here keeps the codebase consistent.

♻️ Proposed change
-    val isDragging by remember(dragAndDropHandler.isDragging) { mutableStateOf(dragAndDropHandler.isDragging) }
+    val isDragging by rememberMutableState(dragAndDropHandler.isDragging) { dragAndDropHandler.isDragging }

Also add to the imports:

+import io.github.composegears.valkyrie.sdk.compose.foundation.rememberMutableState
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackDirectoryPicker.kt`
at line 42, Replace the manual remember { mutableStateOf(...) } usage for
isDragging with the project's helper: use
rememberMutableState(dragAndDropHandler.isDragging) in IconPackDirectoryPicker
(replace the line defining val isDragging by ...), and add the corresponding
import for rememberMutableState to the file so it matches the pattern used in
ChooseExistingPackFile.kt.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/IconPackModeScreen.kt (1)

86-92: Use a more semantically appropriate icon for the Material Pack option

AllIconsKeys.FileTypes.SourceMap is designed for JavaScript source map files and doesn't communicate "Material Design icons" to users. However, reviewing the available IntelliJ platform icons in AllIconsKeys, there are no palette, layers, or design-themed alternatives. Consider using AllIconsKeys.General.Settings or AllIconsKeys.Actions.Properties as an interim solution, or explore if custom icons can be added to represent Material Design more intuitively.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/IconPackModeScreen.kt`
around lines 86 - 92, Replace the semantically incorrect icon
AllIconsKeys.FileTypes.SourceMap used in the InfoCard for the Material Pack
option (InfoCard with title stringResource("iconpack.mode.material.pack.title")
and onClick = onMaterialPack) with a more appropriate interim IntelliJ icon such
as AllIconsKeys.General.Settings or AllIconsKeys.Actions.Properties; update the
icon reference passed to the InfoCard's key parameter and ensure the change is
applied where InfoCard(...) for the Material Pack is declared so the UI better
communicates "Material Design" to users (consider adding a TODO to revisit and
supply a custom Material icon later).
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackScreen.kt (1)

43-53: Optional: Simplify event collection in LaunchedEffect.

flow.onEach { }.launchIn(this) inside a LaunchedEffect launches a redundant nested coroutine — the LaunchedEffect block itself becomes a fire-and-forget launcher. A direct collect is cleaner and avoids the extra coroutine layer:

♻️ Proposed simplification
 LaunchedEffect(Unit) {
-    viewModel.events
-        .onEach {
-            when (it) {
-                is MaterialPackEvent.FinishSetup -> {
-                    navController.replace(dest = IconPackConversionScreen)
-                }
-            }
-        }
-        .launchIn(this)
+    viewModel.events.collect {
+        when (it) {
+            is MaterialPackEvent.FinishSetup -> navController.replace(dest = IconPackConversionScreen)
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackScreen.kt`
around lines 43 - 53, The LaunchedEffect is using viewModel.events.onEach { ...
}.launchIn(this), which spawns a redundant nested coroutine; replace that
pattern by directly collecting the flow inside the LaunchedEffect: call
viewModel.events.collect { when (it) { is MaterialPackEvent.FinishSetup ->
navController.replace(dest = IconPackConversionScreen) } } so you remove
onEach(...).launchIn(this) and use collect within the existing LaunchedEffect to
simplify and avoid the extra coroutine layer.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt (1)

39-40: Consider a buffered Channel for the events channel.

Channel() defaults to RENDEZVOUS (capacity = 0): send suspends until a receiver is ready. If saveIconPack() races to send FinishSetup before LaunchedEffect starts collecting (e.g., during recomposition), the sending coroutine in viewModelScope will hang indefinitely. ExistingPackViewModel uses MutableSharedFlow instead — consider aligning or using Channel(Channel.BUFFERED).

♻️ Proposed fix
-private val _events = Channel<MaterialPackEvent>()
+private val _events = Channel<MaterialPackEvent>(Channel.BUFFERED)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`
around lines 39 - 40, The events channel uses an unbuffered Channel (private val
_events = Channel<MaterialPackEvent>), which can suspend senders (e.g., in
saveIconPack() running in viewModelScope) if no collector (LaunchedEffect) is
ready; change this to a buffered channel or align with ExistingPackViewModel by
replacing the channel with Channel(Channel.BUFFERED) (or switch to a
MutableSharedFlow/SharedFlow) so sending FinishSetup (MaterialPackEvent) cannot
hang, and keep the public events as _events.receiveAsFlow() to preserve the API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/idea-plugin/CHANGELOG.md`:
- Line 8: Change the past-tense entry "Added separate \"Material pack\" option"
to imperative mood to match the rest of the changelog; replace that text with
"Add separate \"Material pack\" option" so the entry uses the same verb tense as
the "Add ..." entries.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/tooling/PreviewTheme.kt`:
- Around line 36-37: Move the side-effectful calls out of the composable body
and into a DisposableEffect: call GlobalPreviewState.isPreview = true and
DI.initWith(project) inside DisposableEffect tied to project (or Unit) and in
its onDispose set GlobalPreviewState.isPreview = false (and optionally perform
any DI cleanup if available); this ensures DI.initWith(project) runs under
Compose effect tracking and GlobalPreviewState.isPreview is reset on disposal.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ExistingPackViewModel.kt`:
- Around line 119-130: The call to IconPackWriter.savePack in
ExistingPackViewModel is relying on the default writeToFile=true, which creates
an implicit divergence from MaterialPackViewModel (which passes
writeToFile=false); update the IconPackWriter.savePack invocation inside
ExistingPackViewModel.viewModelScope.launch to explicitly pass writeToFile =
true (and do the same in NewPackViewModel where applicable) so the behavior is
explicit and consistent with MaterialPackViewModel's explicit writeToFile=false
usage.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackViewModel.kt`:
- Line 40: Change the mutable property inputHandler to an immutable val since
it's only initialized once and never reassigned; locate the declaration "private
var inputHandler = BasicInputHandler(InputFieldState.Empty)" in NewPackViewModel
and replace var with val so you keep the same BasicInputHandler instance while
retaining existing method calls and property accesses on inputHandler.

---

Outside diff comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt`:
- Around line 76-90: The empty-branch renders AddPackButton unconditionally
which bypasses the allowEditing guard used inside NestedPacks; update the
empty-packs branch in IconPackEditor so AddPackButton is only shown when
inputFieldState.allowEditing is true (e.g., wrap the Spacer + AddPackButton
rendering in a check for inputFieldState.allowEditing or render a
disabled/hidden placeholder when false) so behavior matches the NestedPacks
path; reference the AddPackButton and inputFieldState.allowEditing in your
change.

---

Nitpick comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackDirectoryPicker.kt`:
- Line 42: Replace the manual remember { mutableStateOf(...) } usage for
isDragging with the project's helper: use
rememberMutableState(dragAndDropHandler.isDragging) in IconPackDirectoryPicker
(replace the line defining val isDragging by ...), and add the corresponding
import for rememberMutableState to the file so it matches the pattern used in
ChooseExistingPackFile.kt.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/inputhandler/InputHandler.kt`:
- Around line 30-33: The nested pack ID generation in BasicInputHandler
currently uses nestedPacks.size.toString(), which can create duplicate IDs after
removals; change the creation of NestedPack in the onAddNestedPack flow to use a
unique ID generator (match IconPackEditorPreview) such as
Random.nextLong().toString() or UUID.randomUUID().toString() instead of
nestedPacks.size.toString() so each NestedPack.id is globally unique and
removeNestedPack (which filters by id) will only remove the intended pack.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/IconPackModeScreen.kt`:
- Around line 86-92: Replace the semantically incorrect icon
AllIconsKeys.FileTypes.SourceMap used in the InfoCard for the Material Pack
option (InfoCard with title stringResource("iconpack.mode.material.pack.title")
and onClick = onMaterialPack) with a more appropriate interim IntelliJ icon such
as AllIconsKeys.General.Settings or AllIconsKeys.Actions.Properties; update the
icon reference passed to the InfoCard's key parameter and ensure the change is
applied where InfoCard(...) for the Material Pack is declared so the UI better
communicates "Material Design" to users (consider adding a TODO to revisit and
supply a custom Material icon later).

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackScreen.kt`:
- Around line 43-53: The LaunchedEffect is using viewModel.events.onEach { ...
}.launchIn(this), which spawns a redundant nested coroutine; replace that
pattern by directly collecting the flow inside the LaunchedEffect: call
viewModel.events.collect { when (it) { is MaterialPackEvent.FinishSetup ->
navController.replace(dest = IconPackConversionScreen) } } so you remove
onEach(...).launchIn(this) and use collect within the existing LaunchedEffect to
simplify and avoid the extra coroutine layer.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`:
- Around line 39-40: The events channel uses an unbuffered Channel (private val
_events = Channel<MaterialPackEvent>), which can suspend senders (e.g., in
saveIconPack() running in viewModelScope) if no collector (LaunchedEffect) is
ready; change this to a buffered channel or align with ExistingPackViewModel by
replacing the channel with Channel(Channel.BUFFERED) (or switch to a
MutableSharedFlow/SharedFlow) so sending FinishSetup (MaterialPackEvent) cannot
hang, and keep the public events as _events.receiveAsFlow() to preserve the API.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt`:
- Line 51: Replace the reused NewPack string key with a dedicated Material Pack
save key: add a new resource entry (e.g. "iconpack.materialpack.creation.save"
-> "Save" or final copy) to the strings bundle, then update the Text composable
call that currently uses stringResource("iconpack.newpack.creation.continue") to
use stringResource("iconpack.materialpack.creation.save") instead; locate the
usage in MaterialPackCreation (the Text/stringResource call) and ensure the new
key is added to the appropriate localization file.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackViewModel.kt`:
- Around line 42-43: The Channel used for event delivery (_events of type
Channel<NewPackEvent> in NewPackViewModel) is rendezvous by default and can
suspend senders during brief gaps in collection; change its construction to a
buffered channel (for example using Channel(Channel.BUFFERED) or a small fixed
capacity like Channel(1)) so _events.send(...) won't suspend if the collector is
not immediately ready, and keep exposing events via events =
_events.receiveAsFlow().

Comment thread tools/idea-plugin/CHANGELOG.md Outdated
@egorikftp egorikftp force-pushed the feature/material-pack branch 2 times, most recently from f56af14 to 88d872c Compare February 18, 2026 19:45
@egorikftp
Copy link
Copy Markdown
Member Author

@coderabbitai review all

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

@egorikftp: I'll review all the changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt (1)

76-90: ⚠️ Potential issue | 🟡 Minor

Code inconsistency: allowEditing not checked when nestedPacks is empty.

When nestedPacks is empty (line 76), AddPackButton is rendered without checking inputFieldState.allowEditing. When nestedPacks is non-empty, allowEditing gates the button (line 135). While Material Pack always pre-populates nested packs (avoiding this scenario), the inconsistency leaves the code fragile. For consistency and defensive coding, apply the check in both branches.

Proposed fix
         if (inputFieldState.nestedPacks.isEmpty()) {
-            Spacer(8.dp)
-            AddPackButton(
-                modifier = Modifier.align(Alignment.Start),
-                onClick = onAddNestedPack,
-            )
+            if (inputFieldState.allowEditing) {
+                Spacer(8.dp)
+                AddPackButton(
+                    modifier = Modifier.align(Alignment.Start),
+                    onClick = onAddNestedPack,
+                )
+            }
         } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt`
around lines 76 - 90, The branch that renders AddPackButton when
inputFieldState.nestedPacks.isEmpty() should also respect
inputFieldState.allowEditing; change the empty-branch to only render
AddPackButton when inputFieldState.allowEditing is true (same gating used when
nestedPacks is non-empty) so AddPackButton is not shown or clickable when
editing is disallowed, keeping behavior consistent between the empty and
non-empty branches; update the conditional around AddPackButton (referencing
inputFieldState, allowEditing, AddPackButton, onAddNestedPack, and NestedPacks)
accordingly.
🧹 Nitpick comments (6)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialInputHandler.kt (1)

26-32: Consider using semantic IDs for NestedPack instead of sequential integers.

The IDs "0""4" are unambiguous since the list is static and non-removable, but using the icon set name as the ID (matching the text value) is more self-documenting and easier to reason about if IDs surface in logs or error paths.

♻️ Suggested refactor
-                    NestedPack(id = "0", inputFieldState = InputState(text = "Filled", enabled = false)),
-                    NestedPack(id = "1", inputFieldState = InputState(text = "Outlined", enabled = false)),
-                    NestedPack(id = "2", inputFieldState = InputState(text = "Rounded", enabled = false)),
-                    NestedPack(id = "3", inputFieldState = InputState(text = "TwoTone", enabled = false)),
-                    NestedPack(id = "4", inputFieldState = InputState(text = "Sharp", enabled = false)),
+                    NestedPack(id = "filled", inputFieldState = InputState(text = "Filled", enabled = false)),
+                    NestedPack(id = "outlined", inputFieldState = InputState(text = "Outlined", enabled = false)),
+                    NestedPack(id = "rounded", inputFieldState = InputState(text = "Rounded", enabled = false)),
+                    NestedPack(id = "twoTone", inputFieldState = InputState(text = "TwoTone", enabled = false)),
+                    NestedPack(id = "sharp", inputFieldState = InputState(text = "Sharp", enabled = false)),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialInputHandler.kt`
around lines 26 - 32, Replace the numeric string IDs in the nestedPacks list
with semantic IDs that match the icon set names; update the NestedPack entries
inside nestedPacks (the list containing NestedPack(...) with inputFieldState =
InputState(...)) so each NestedPack.id uses the corresponding text value
("Filled", "Outlined", "Rounded", "TwoTone", "Sharp") instead of "0".."4" to
make IDs self-descriptive and match InputState.text.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackScreen.kt (1)

134-141: Preview duplicates the DirectoryState initialization literal.

The same DirectoryState(iconPackDestination = "path/to/import", predictedPackage = "com.example.iconpack", nextAvailable = true) appears in both the initial state (line 136) and the onBack handler (line 180). Consider extracting a preview constant to avoid drift between the two.

Also applies to: 179-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackScreen.kt`
around lines 134 - 141, The DirectoryState literal is duplicated in the initial
state and the onBack handler; extract it to a shared immutable preview constant
(e.g., PREVIEW_DIRECTORY_STATE or previewDirectoryState) and use that constant
when constructing ChooseDestinationDirectoryState in rememberMutableState and
inside the onBack handler; update references to
DirectoryState(iconPackDestination = "path/to/import", predictedPackage =
"com.example.iconpack", nextAvailable = true) so both NewPackModeState creation
and the onBack use the single constant (refer to rememberMutableState,
ChooseDestinationDirectoryState, DirectoryState, and the onBack handler).
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ExistingPackViewModel.kt (1)

49-50: MutableSharedFlow for events is inconsistent with sibling ViewModels and can silently drop events.

NewPackViewModel and MaterialPackViewModel were migrated to Channel + receiveAsFlow() in this PR, which suspends send() until the collector is ready — guaranteeing delivery. ExistingPackViewModel still uses MutableSharedFlow(replay=0), meaning emit() completes immediately even with no subscriber, potentially dropping navigation events (e.g., OnSettingsUpdated, PreviewIconPackObject) if the UI collector is transiently unsubscribed.

♻️ Suggested fix
-    private val _events = MutableSharedFlow<ExistingPackEvent>()
-    val events = _events.asSharedFlow()
+    private val _events = Channel<ExistingPackEvent>()
+    val events = _events.receiveAsFlow()

And replace all _events.emit(...) calls with _events.send(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ExistingPackViewModel.kt`
around lines 49 - 50, Replace the MutableSharedFlow-based event bus in
ExistingPackViewModel with a rendezvous Channel to match siblings: change the
backing _events from MutableSharedFlow<ExistingPackEvent>() to a
Channel<ExistingPackEvent>(Channel.RENDEZVOUS) (or simply Channel()), expose
events as _events.receiveAsFlow(), and update all places that call
_events.emit(...) to use _events.send(...) so sends suspend until a collector is
ready; refer to symbols ExistingPackViewModel, _events, events, and
ExistingPackEvent when making the edits.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt (2)

44-46: No-op onAddNestedPack/onRemoveNestedPack are safe but should be guarded if the state could have empty nestedPacks.

When inputFieldState.nestedPacks.isEmpty(), IconPackEditor shows AddPackButton wired to onAddNestedPack = {} — clicking it silently does nothing. For the current Material pack state this is unreachable (Material icons always have nested packs), but it's a latent UX dead-end if the state ever has empty packs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt`
around lines 44 - 46, The IconPackEditor is being passed no-op lambdas for
onAddNestedPack/onRemoveNestedPack which leads to a dead-end when
inputFieldState.nestedPacks.isEmpty(); update MaterialPackCreation so it
guards/handles empty nestedPacks by providing a real handler or conditional UI:
either supply a concrete onAddNestedPack implementation that inserts a default
nested pack into inputFieldState.nestedPacks (or calls the existing state
updater) and a matching onRemoveNestedPack, or wrap the IconPackEditor call with
a check on inputFieldState.nestedPacks.isEmpty() to show an alternative
Add-first UI/button that calls the pack-adding logic; refer to IconPackEditor,
AddPackButton, onAddNestedPack, onRemoveNestedPack and
inputFieldState.nestedPacks to locate where to add the guard/implementation.

56-56: Reusing "iconpack.newpack.creation.continue" string key in the Material pack flow — consider a dedicated key.

The button label is borrowed from the New Pack namespace. As long as the text is identical, this works — but it creates a hidden coupling: a future New Pack wording change would silently affect Material Pack UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt`
at line 56, The MaterialPackCreation composable currently reuses the
"iconpack.newpack.creation.continue" string key which creates a hidden coupling;
replace that key with a dedicated one (e.g., "materialpack.creation.continue")
and update the strings resource file(s) accordingly, then change the Text(...)
call in MaterialPackCreation to use the new key so the Material pack flow has
its own independent localization entry.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt (1)

83-101: updateDestinationPath and saveDestination are near-identical to NewPackViewModel — consider extracting shared logic.

Both ViewModels implement structurally identical functions (same PackageExtractor call, same DirectoryState construction, same inMemorySettings.update pattern). The only difference is NewPackViewModel.saveDestination also sets flatPackage = false. This duplication will need to be kept in sync as the flows evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`
around lines 83 - 101, Extract the duplicated logic into a shared helper: create
a function (e.g., buildDirectoryState(path: Path): DirectoryState) that uses
PackageExtractor.getFrom(path.invariantSeparatorsPathString).orEmpty() and
path.absolutePathString(), then have updateDestinationPath use that to set
ChooseDestinationDirectoryState; likewise extract a
saveDestinationToSettings(directoryState: DirectoryState, setFlatPackage:
Boolean = false) that calls inMemorySettings.update { iconPackDestination =
directoryState.iconPackDestination; if (setFlatPackage) flatPackage = false },
and replace both updateDestinationPath and saveDestination to call these helpers
(ensure NewPackViewModel.saveDestination passes setFlatPackage = true to
preserve its difference); reference functions: updateDestinationPath,
saveDestination, NewPackViewModel.saveDestination, PackageExtractor,
DirectoryState, ChooseDestinationDirectoryState, inMemorySettings.update,
currentState.safeAs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/button/OutlineIconButton.kt`:
- Around line 53-56: Replace the disabled icon tint selection that currently
reads the button border color and unsafe-casts it with a semantic content color:
in OutlineIconButton (the tint variable/when expression that references
style.colors.borderDisabled and safeAs<SolidColor>()), use
style.colors.contentDisabled directly (with a fallback to Color.Unspecified)
when enabled is false so the icon uses the proper disabled foreground color and
you avoid brittle Brush casting.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackViewModel.kt`:
- Line 45: The ViewModel's initial _state is being set to a blank
ChooseDestinationDirectoryState(), which regresses restoration of persisted
destination data; change the initialization of _state
(MutableStateFlow<NewPackModeState>) to use the persisted settings by calling
inMemorySettings.current.toChooseDirectoryState() so the initial
NewPackModeState is populated with saved iconPackDestination, predictedPackage,
and nextAvailable values instead of empty defaults.

---

Outside diff comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt`:
- Around line 76-90: The branch that renders AddPackButton when
inputFieldState.nestedPacks.isEmpty() should also respect
inputFieldState.allowEditing; change the empty-branch to only render
AddPackButton when inputFieldState.allowEditing is true (same gating used when
nestedPacks is non-empty) so AddPackButton is not shown or clickable when
editing is disallowed, keeping behavior consistent between the empty and
non-empty branches; update the conditional around AddPackButton (referencing
inputFieldState, allowEditing, AddPackButton, onAddNestedPack, and NestedPacks)
accordingly.

---

Duplicate comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/tooling/PreviewTheme.kt`:
- Around line 36-37: Both assignments are side effects executed on every
recomposition: wrap GlobalPreviewState.isPreview = true and DI.initWith(project)
inside a DisposableEffect keyed to project (or Unit) within the composable so
they run once when entered and are cleaned up on disposal; set
GlobalPreviewState.isPreview = true in the effect and reset it to false in
DisposableEffect.onDispose, and call DI.initWith(project) in the effect and
perform any necessary DI cleanup in onDispose (refer to
GlobalPreviewState.isPreview and DI.initWith(project) to locate code).

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ExistingPackViewModel.kt`:
- Around line 120-125: The call in ExistingPackViewModel to
IconPackWriter.savePack inside the withContext(Dispatchers.IO) block relies on
the default writeToFile value (true); make this explicit by adding writeToFile =
false to the savePack call (matching MaterialPackViewModel and
NewPackViewModel), so update the IconPackWriter.savePack invocation in
ExistingPackViewModel to pass writeToFile = false.

---

Nitpick comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ExistingPackViewModel.kt`:
- Around line 49-50: Replace the MutableSharedFlow-based event bus in
ExistingPackViewModel with a rendezvous Channel to match siblings: change the
backing _events from MutableSharedFlow<ExistingPackEvent>() to a
Channel<ExistingPackEvent>(Channel.RENDEZVOUS) (or simply Channel()), expose
events as _events.receiveAsFlow(), and update all places that call
_events.emit(...) to use _events.send(...) so sends suspend until a collector is
ready; refer to symbols ExistingPackViewModel, _events, events, and
ExistingPackEvent when making the edits.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialInputHandler.kt`:
- Around line 26-32: Replace the numeric string IDs in the nestedPacks list with
semantic IDs that match the icon set names; update the NestedPack entries inside
nestedPacks (the list containing NestedPack(...) with inputFieldState =
InputState(...)) so each NestedPack.id uses the corresponding text value
("Filled", "Outlined", "Rounded", "TwoTone", "Sharp") instead of "0".."4" to
make IDs self-descriptive and match InputState.text.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`:
- Around line 83-101: Extract the duplicated logic into a shared helper: create
a function (e.g., buildDirectoryState(path: Path): DirectoryState) that uses
PackageExtractor.getFrom(path.invariantSeparatorsPathString).orEmpty() and
path.absolutePathString(), then have updateDestinationPath use that to set
ChooseDestinationDirectoryState; likewise extract a
saveDestinationToSettings(directoryState: DirectoryState, setFlatPackage:
Boolean = false) that calls inMemorySettings.update { iconPackDestination =
directoryState.iconPackDestination; if (setFlatPackage) flatPackage = false },
and replace both updateDestinationPath and saveDestination to call these helpers
(ensure NewPackViewModel.saveDestination passes setFlatPackage = true to
preserve its difference); reference functions: updateDestinationPath,
saveDestination, NewPackViewModel.saveDestination, PackageExtractor,
DirectoryState, ChooseDestinationDirectoryState, inMemorySettings.update,
currentState.safeAs.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt`:
- Around line 44-46: The IconPackEditor is being passed no-op lambdas for
onAddNestedPack/onRemoveNestedPack which leads to a dead-end when
inputFieldState.nestedPacks.isEmpty(); update MaterialPackCreation so it
guards/handles empty nestedPacks by providing a real handler or conditional UI:
either supply a concrete onAddNestedPack implementation that inserts a default
nested pack into inputFieldState.nestedPacks (or calls the existing state
updater) and a matching onRemoveNestedPack, or wrap the IconPackEditor call with
a check on inputFieldState.nestedPacks.isEmpty() to show an alternative
Add-first UI/button that calls the pack-adding logic; refer to IconPackEditor,
AddPackButton, onAddNestedPack, onRemoveNestedPack and
inputFieldState.nestedPacks to locate where to add the guard/implementation.
- Line 56: The MaterialPackCreation composable currently reuses the
"iconpack.newpack.creation.continue" string key which creates a hidden coupling;
replace that key with a dedicated one (e.g., "materialpack.creation.continue")
and update the strings resource file(s) accordingly, then change the Text(...)
call in MaterialPackCreation to use the new key so the Material pack flow has
its own independent localization entry.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackScreen.kt`:
- Around line 134-141: The DirectoryState literal is duplicated in the initial
state and the onBack handler; extract it to a shared immutable preview constant
(e.g., PREVIEW_DIRECTORY_STATE or previewDirectoryState) and use that constant
when constructing ChooseDestinationDirectoryState in rememberMutableState and
inside the onBack handler; update references to
DirectoryState(iconPackDestination = "path/to/import", predictedPackage =
"com.example.iconpack", nextAvailable = true) so both NewPackModeState creation
and the onBack use the single constant (refer to rememberMutableState,
ChooseDestinationDirectoryState, DirectoryState, and the onBack handler).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/IconPackModeScreen.kt (1)

87-87: Optional: consider a more semantically appropriate icon for Material pack.

AllIconsKeys.FileTypes.SourceMap is the IDE's icon for JavaScript/TypeScript source-map files, which is thematically unrelated to Material Icons. A few alternatives that carry a "design/components" connotation: AllIconsKeys.Toolwindows.ToolWindowComponents, AllIconsKeys.Actions.Colors, or a custom icon. This has no functional impact.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/IconPackModeScreen.kt`
at line 87, The icon used for the Material pack is currently
AllIconsKeys.FileTypes.SourceMap which is semantically for source maps; update
the icon reference in IconPackModeScreen (the key =
AllIconsKeys.FileTypes.SourceMap usage) to a more appropriate icon such as
AllIconsKeys.Toolwindows.ToolWindowComponents or AllIconsKeys.Actions.Colors, or
swap in a custom Material icon resource; ensure you replace the referenced
symbol only (key assignment) so the rest of IconPackModeScreen remains
unchanged.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackDirectoryPicker.kt (1)

74-78: Refactor string keys to reflect generic usage, not NewPack-specific scope.

The component is used by both NewPackScreen and MaterialPackScreen, but all string references (lines 77, 90, 96, 107) use the iconpack.newpack namespace prefix, which semantically indicates they are NewPack-specific. While the actual text content ("Drag & Drop destination folder", "Destination path", etc.) is generic enough to work for both flows, the string key namespace should be refactored to iconpack.common to align with the component's placement in the common package.

Consider moving the string keys to a generic namespace or parameterizing them so the string scope matches the component's intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackDirectoryPicker.kt`
around lines 74 - 78, The string keys used by IconPackDirectoryPicker (the
stringResource calls in this composable that currently use the
iconpack.newpack.* namespace) are NewPack-specific; refactor them to a generic
namespace (e.g., iconpack.common.choose.directory.dnd,
iconpack.common.destination.path, etc.) or accept string resource keys/strings
as parameters to IconPackDirectoryPicker so the component is truly common;
update the stringResource invocations in IconPackDirectoryPicker to use the new
iconpack.common keys (or the passed-in parameters) and add corresponding entries
to the resources.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt (1)

94-140: Clarify the semantic scope of allowEditing parameter.

The allowEditing flag (line 135) only gates the "Add nested pack" button, but delete buttons for individual packs remain controlled by per-pack inputFieldState.enabled flags. If this layered control is intentional, consider renaming allowEditing to allowAddingPacks for clarity, since it doesn't prevent deletion of existing packs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt`
around lines 94 - 140, The parameter allowEditing on the NestedPacks composable
only controls showing the AddPackButton while deletion remains governed by each
nestedPack.inputFieldState.enabled; rename allowEditing to allowAddingPacks
(update the NestedPacks signature and all call sites) and update any related
KDoc/comments to reflect the narrower semantics, or alternatively change
behavior to gate deletes via allowAddingPacks && inputFieldState.enabled if you
intend a single editing flag; adjust usages of onRemove, AddPackButton, and any
docs to match the new name/behavior.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt (1)

44-45: No-op lambdas for nested pack editing are intentional but could benefit from a brief comment.

Since the material pack flow uses pre-defined, non-editable nested packs (allowEditing = false), the empty lambdas are correct. A brief inline comment would clarify intent for future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt`
around lines 44 - 45, Add a short inline comment beside the empty lambdas
onAddNestedPack and onRemoveNestedPack in MaterialPackCreation.kt explaining
they are intentionally no-ops because nested packs are pre-defined and editing
is disabled (allowEditing = false); reference the onAddNestedPack and
onRemoveNestedPack parameters to locate the exact spot and keep the comment
concise so future readers understand this is intentional, not an omission.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt (1)

41-42: Channel pattern is consistent codebase-wide; buffering not essential but could be considered.

Channel<MaterialPackEvent>() uses RENDEZVOUS capacity (default), matching the pattern across NewPackViewModel, SvgXmlViewModel, and other ViewModels. All send events from active coroutine contexts (user-triggered actions within viewModelScope.launch blocks), so the UI collector is ready when send() is called. Buffering with Channel(capacity = 1) is optional—the RENDEZVOUS pattern is intentional for one-shot event delivery and works reliably in this context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`
around lines 41 - 42, The Channel in MaterialPackViewModel using the default
RENDEZVOUS capacity is intentional and consistent with other view models; update
the code by adding a brief comment near the _events/ events declarations
(referencing _events: Channel<MaterialPackEvent>(), events =
_events.receiveAsFlow(), and the MaterialPackViewModel) stating that RENDEZVOUS
is chosen intentionally for one-shot event delivery (matching NewPackViewModel,
SvgXmlViewModel) and that buffering is optional because sends occur from
viewModelScope coroutines where the UI collector is expected to be ready.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/button/OutlineIconButton.kt`:
- Around line 53-56: The disabled icon tint logic in OutlineIconButton.kt
currently reads style.colors.borderDisabled and casts to SolidColor, which is
wrong and brittle; change the tint computation to use
style.colors.contentDisabled (no cast to SolidColor and no fallback to
Color.Unspecified) so the icon uses the button's disabled foreground color;
after updating the tint expression, remove any now-unused SolidColor and safeAs
imports.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/tooling/PreviewTheme.kt`:
- Around line 36-37: Move the side-effectful calls out of the composable body
and into a DisposableEffect keyed appropriately: set
GlobalPreviewState.isPreview = true inside a DisposableEffect and restore it to
false in onDispose, and call DI.initWith(project) once inside the same (or a
separate) DisposableEffect so it runs only on composition entry and can be
cleaned up on dispose; update the PreviewTheme composable to wrap these
assignments in DisposableEffect blocks referencing the project (or Unit if
project is stable) and perform any necessary DI shutdown/cleanup in onDispose.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`:
- Around line 109-125: saveIconPack in MaterialPackViewModel is correct to call
IconPackWriter.savePack(writeToFile = false) and set
inMemorySettings.flatPackage = true; ensure this explicit behavior is preserved
here and make the other view models symmetric by making their intent explicit:
in NewPackViewModel and ExistingPackViewModel call inMemorySettings.update {
flatPackage = false } before invoking IconPackWriter.savePack and pass
writeToFile = true explicitly to savePack, so the difference between material
packs and regular packs is clear and not implicit.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackViewModel.kt`:
- Around line 45-46: The initial _state is created with a blank
ChooseDestinationDirectoryState() which ignores persisted destination; update
the NewPackViewModel initialization so _state =
MutableStateFlow<NewPackModeState>(...) is seeded from inMemorySettings.current
(use its iconPackDestination and any derived predictedPackage/nextAvailable)
instead of empty defaults, e.g. construct a ChooseDestinationDirectoryState
populated from inMemorySettings.current values so returning users see their
saved output folder and derived fields when the ViewModel is created.

---

Nitpick comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackDirectoryPicker.kt`:
- Around line 74-78: The string keys used by IconPackDirectoryPicker (the
stringResource calls in this composable that currently use the
iconpack.newpack.* namespace) are NewPack-specific; refactor them to a generic
namespace (e.g., iconpack.common.choose.directory.dnd,
iconpack.common.destination.path, etc.) or accept string resource keys/strings
as parameters to IconPackDirectoryPicker so the component is truly common;
update the stringResource invocations in IconPackDirectoryPicker to use the new
iconpack.common keys (or the passed-in parameters) and add corresponding entries
to the resources.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/common/IconPackEditor.kt`:
- Around line 94-140: The parameter allowEditing on the NestedPacks composable
only controls showing the AddPackButton while deletion remains governed by each
nestedPack.inputFieldState.enabled; rename allowEditing to allowAddingPacks
(update the NestedPacks signature and all call sites) and update any related
KDoc/comments to reflect the narrower semantics, or alternatively change
behavior to gate deletes via allowAddingPacks && inputFieldState.enabled if you
intend a single editing flag; adjust usages of onRemove, AddPackButton, and any
docs to match the new name/behavior.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/IconPackModeScreen.kt`:
- Line 87: The icon used for the Material pack is currently
AllIconsKeys.FileTypes.SourceMap which is semantically for source maps; update
the icon reference in IconPackModeScreen (the key =
AllIconsKeys.FileTypes.SourceMap usage) to a more appropriate icon such as
AllIconsKeys.Toolwindows.ToolWindowComponents or AllIconsKeys.Actions.Colors, or
swap in a custom Material icon resource; ensure you replace the referenced
symbol only (key assignment) so the rest of IconPackModeScreen remains
unchanged.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`:
- Around line 41-42: The Channel in MaterialPackViewModel using the default
RENDEZVOUS capacity is intentional and consistent with other view models; update
the code by adding a brief comment near the _events/ events declarations
(referencing _events: Channel<MaterialPackEvent>(), events =
_events.receiveAsFlow(), and the MaterialPackViewModel) stating that RENDEZVOUS
is chosen intentionally for one-shot event delivery (matching NewPackViewModel,
SvgXmlViewModel) and that buffering is optional because sends occur from
viewModelScope coroutines where the UI collector is expected to be ready.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/ui/MaterialPackCreation.kt`:
- Around line 44-45: Add a short inline comment beside the empty lambdas
onAddNestedPack and onRemoveNestedPack in MaterialPackCreation.kt explaining
they are intentionally no-ops because nested packs are pre-defined and editing
is disabled (allowEditing = false); reference the onAddNestedPack and
onRemoveNestedPack parameters to locate the exact spot and keep the comment
concise so future readers understand this is intentional, not an omission.

@egorikftp egorikftp force-pushed the feature/material-pack branch from 88d872c to d60df29 Compare February 19, 2026 08:06
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackScreen.kt (1)

64-98: Consider extracting the shared screen structure between NewPackContent and MaterialPackContent.

Both MaterialPackContent and NewPackContent (in NewPackScreen.kt) share a near-identical structural pattern: Column { Toolbar { BackAction; Title } VerticallyScrollableContainer { Column(centered) { when(state) { DirectoryPicker; Creation } } } }. The main differences are the title string, toolbar extras, and the specific creation composable. A shared scaffold composable could reduce this duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackScreen.kt`
around lines 64 - 98, Extract the repeated layout in MaterialPackContent and
NewPackContent into a shared composable (e.g., PackScreenScaffold) that accepts
parameters for title (stringResource id), optional toolbar extras (slot lambda),
and a body lambda that receives the screen state and callbacks; update
MaterialPackContent to call PackScreenScaffold supplying the title
stringResource("iconpack.material.title"), any toolbar extras, and a body that
switches on MaterialPackState to call IconPackDirectoryPicker and
MaterialPackCreation with the same callbacks (onAction, onValueChange), and do
the analogous change in NewPackContent to reuse the same scaffold but pass its
title and NewPackCreation composable – keep existing symbols
IconPackDirectoryPicker, MaterialPackCreation, NewPackContent,
MaterialPackContent and the action/callback signatures intact.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/model/ExistingPackModeState.kt (1)

9-16: Consistent with the broader state model migration.

The replacement of PackEditState with InputFieldState and the default construction pattern align with the same approach used in NewPackModeState.PickedState and ExistingPackInputHandler.

One observation: the default InputFieldState(iconPackName = InputState(), packageName = InputState(), nestedPacks = emptyList()) construction is repeated verbatim in at least three locations (NewPackModeState, ExistingPackModeState, ExistingPackInputHandler). Consider extracting an InputFieldState.Empty companion constant to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/model/ExistingPackModeState.kt`
around lines 9 - 16, Introduce a shared constant to avoid repeated construction
of the same empty InputFieldState: add a companion object on InputFieldState
with val Empty = InputFieldState(iconPackName = InputState(), packageName =
InputState(), nestedPacks = emptyList()), then replace the inline constructions
in ExistingPackEditState (ExistingPackEditState.inputFieldState),
NewPackModeState.PickedState, and ExistingPackInputHandler with
InputFieldState.Empty so they all reference the single canonical empty instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ExistingPackViewModel.kt`:
- Around line 111-128: The IconPackWriter.savePack call in
ExistingPackViewModel.saveIconPack relies on the default writeToFile value; make
this explicit to avoid divergence with MaterialPackViewModel. Update the call in
saveIconPack to pass writeToFile = true (or the appropriate boolean to match
intended behavior) so the parameter is explicit—refer to the
IconPackWriter.savePack invocation in ExistingPackViewModel.saveIconPack and the
analogous explicit writeToFile = false usage in MaterialPackViewModel to align
behavior.

---

Nitpick comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/model/ExistingPackModeState.kt`:
- Around line 9-16: Introduce a shared constant to avoid repeated construction
of the same empty InputFieldState: add a companion object on InputFieldState
with val Empty = InputFieldState(iconPackName = InputState(), packageName =
InputState(), nestedPacks = emptyList()), then replace the inline constructions
in ExistingPackEditState (ExistingPackEditState.inputFieldState),
NewPackModeState.PickedState, and ExistingPackInputHandler with
InputFieldState.Empty so they all reference the single canonical empty instance.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackScreen.kt`:
- Around line 64-98: Extract the repeated layout in MaterialPackContent and
NewPackContent into a shared composable (e.g., PackScreenScaffold) that accepts
parameters for title (stringResource id), optional toolbar extras (slot lambda),
and a body lambda that receives the screen state and callbacks; update
MaterialPackContent to call PackScreenScaffold supplying the title
stringResource("iconpack.material.title"), any toolbar extras, and a body that
switches on MaterialPackState to call IconPackDirectoryPicker and
MaterialPackCreation with the same callbacks (onAction, onValueChange), and do
the analogous change in NewPackContent to reuse the same scaffold but pass its
title and NewPackCreation composable – keep existing symbols
IconPackDirectoryPicker, MaterialPackCreation, NewPackContent,
MaterialPackContent and the action/callback signatures intact.

@egorikftp egorikftp force-pushed the feature/material-pack branch from 1437862 to f5a5721 Compare February 19, 2026 15:07
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt (1)

67-74: ⚠️ Potential issue | 🟡 Minor

Enter key may double-invoke onValueChange via the new blur path.

focusManager.clearFocus() is called before onValueChange in the Enter branch. Because Compose defers recomposition until after the current event handler returns, currentText (via rememberUpdatedState) still holds the old value when the blur callback fires synchronously inside clearFocus(). The blur block therefore sees newText != currentText and calls onValueChange — then the Enter branch calls it a second time with the same value.

While calling onValueChange twice with the same string is idempotent in most cases, it's an unintended side-effect of the new blur-commit logic. The cleanest fix is to guard the blur commit with a flag set before clearFocus() is called from any explicit-commit path.

♻️ Suggested fix using a commit-in-progress guard
+    var committingExplicitly by rememberMutableState { false }

     TextField(
         modifier = modifier
             .heightIn(min = 32.dp)
             .onFocusChanged {
                 if (focused && !it.isFocused && !isError) {
+                    if (!committingExplicitly) {
                         val newText = state.text.toString()
                         if (newText != currentText) {
                             onValueChange(newText)
                         }
+                    }
                 }
                 focused = it.isFocused
             }
             .onKeyEvent {
                 when (it.key) {
                     Key.Enter -> {
                         if (!isError) {
+                            committingExplicitly = true
                             focusManager.clearFocus()
                             onValueChange(state.text.toString())
+                            committingExplicitly = false
                             true
                         } else {
                             false
                         }
                     }

Apply the same guard to the checkmark button's onClick for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt`
around lines 67 - 74, The Enter key handler in ConfirmTextField (Key.Enter
branch) calls focusManager.clearFocus() before invoking onValueChange, causing
the blur callback (which compares newText against currentText from
rememberUpdatedState) to also call onValueChange and double-invoke it; add a
commit-in-progress boolean flag (e.g., isCommitting) stored in the component
scope, set it true immediately before calling focusManager.clearFocus() in the
Key.Enter path and in the checkmark button's onClick, then have the blur
callback check this flag and skip its onValueChange commit when isCommitting is
true (reset the flag after committing) so explicit-commit paths do not trigger
the blur commit.
🧹 Nitpick comments (5)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackInputHandler.kt (1)

9-34: Clean separation of the new-pack flow from the material-pack flow.

The old NewPackInputHandler had useMaterialPack branching with hardcoded Material Icons configuration — that responsibility is now properly split into a dedicated MaterialInputHandler. This handler is focused and simple.

One observation: invalidateInputFieldState (lines 13–23) is identical to MaterialInputHandler.invalidateInputFieldState (same "preserve user's packageName, fill from destination if empty" logic). If these are expected to stay in sync, consider extracting a shared utility. Not a blocker since they may diverge independently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackInputHandler.kt`
around lines 9 - 34, The invalidateInputFieldState implementation in
NewPackInputHandler duplicates the same "preserve user's packageName, fill from
destination if empty" logic found in
MaterialInputHandler.invalidateInputFieldState; extract that shared logic into a
small utility (e.g., a function named resolvePackageNameText or
resolvePackageNameState) and call it from both
NewPackInputHandler.invalidateInputFieldState and
MaterialInputHandler.invalidateInputFieldState so they stay in sync without
duplicated code; update the companion object usage if needed to reuse
PackageExtractor.getFrom(iconPackDestination) via the new utility and keep the
handlers focused on their specific state differences.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt (4)

102-108: saveDestination() doesn't reset flatPackage — diverges from NewPackViewModel.

NewPackViewModel.saveDestination() explicitly sets flatPackage = false when persisting the destination. MaterialPackViewModel omits this, leaving whatever flatPackage value a previous session wrote. saveIconPack() does correct this before writing, so the risk is limited to any code path that reads inMemorySettings.flatPackage between saveDestination() and saveIconPack(). Consider whether resetting flatPackage here — or at least documenting the deliberate omission — is appropriate.

♻️ Proposed alignment with `NewPackViewModel`
 private fun saveDestination() {
     val destinationDirectoryState = currentState.safeAs<ChooseDestinationDirectoryState>() ?: return

     inMemorySettings.update {
         iconPackDestination = destinationDirectoryState.directoryState.iconPackDestination
+        flatPackage = false
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`
around lines 102 - 108, saveDestination() in MaterialPackViewModel updates
iconPackDestination but doesn't reset inMemorySettings.flatPackage like
NewPackViewModel.saveDestination() does, leaving stale values between
saveDestination() and saveIconPack(); update
MaterialPackViewModel.saveDestination() to explicitly set
inMemorySettings.flatPackage = false (or document deliberate omission) so the
saved destination state matches NewPackViewModel behavior and avoids reading
stale flatPackage before saveIconPack(), referencing saveDestination(),
saveIconPack(), inMemorySettings.flatPackage, and class MaterialPackViewModel
when making the change.

90-100: updateDestinationPath is duplicated from NewPackViewModel.

The implementation (lines 90–100) is character-for-character identical to NewPackViewModel.updateDestinationPath (lines 83–93 in the snippet). Extracting this into a shared utility (e.g., on DirectoryState's companion or a PackDestinationHelper) would eliminate the duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`
around lines 90 - 100, Extract the duplicate logic from
MaterialPackViewModel.updateDestinationPath and
NewPackViewModel.updateDestinationPath into a shared helper (either a
DirectoryState companion factory or a new PackDestinationHelper function) that
takes a Path and returns the ChooseDestinationDirectoryState/DirectoryState
initialized with iconPackDestination, predictedPackage (using
PackageExtractor.getFrom(...).orEmpty()), and nextAvailable = true; then replace
both MaterialPackViewModel.updateDestinationPath and
NewPackViewModel.updateDestinationPath to call that shared utility and update
_state with its result.

42-43: Consider buffering the _events channel to avoid losing FinishSetup on slow collectors.

Channel<MaterialPackEvent>() creates an unbuffered (rendezvous) channel. Channel.RENDEZVOUS has no buffer at all; send suspends until a receiver invokes receive. If the UI collector is momentarily inactive when _events.send(FinishSetup) is called (e.g., during a recomposition or brief suspension), the event will be held until the collector resumes — or dropped if the ViewModel scope is cancelled first. A Channel(Channel.BUFFERED) would eliminate this race.

♻️ Use a buffered channel
-private val _events = Channel<MaterialPackEvent>()
+private val _events = Channel<MaterialPackEvent>(Channel.BUFFERED)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`
around lines 42 - 43, The _events Channel is currently unbuffered which can drop
or suspend sends like _events.send(FinishSetup) if the UI collector is briefly
inactive; change its creation from Channel<MaterialPackEvent>() to a buffered
channel (e.g., Channel(Channel.BUFFERED) or Channel(capacity)) so events are
queued for short-lived suspensions, keeping the existing events =
_events.receiveAsFlow() and MaterialPackEvent/FinishSetup logic intact.

116-131: Add a comment explaining why writeToFile = false for Material packs.

The writeToFile = false parameter is intentional: Material Design Icons are imported from an external library dependency rather than generated. Unlike NewPackViewModel and ExistingPackViewModel which create/modify icon files on disk, MaterialPackViewModel only updates configuration settings. Consider adding a brief inline comment to clarify this for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`
around lines 116 - 131, In saveIconPack (MaterialPackViewModel) add a brief
inline comment next to the IconPackWriter.savePack call explaining that
writeToFile = false is intentional because Material packs use icons from an
external Material Design library dependency (no icon files are generated on
disk) and this view model only updates inMemorySettings/configuration rather
than writing icon files; reference the saveIconPack method and the writeToFile =
false flag so future maintainers understand the difference from
NewPackViewModel/ExistingPackViewModel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt`:
- Around line 51-57: In ConfirmTextField the Escape key handler currently calls
focusManager.clearFocus() before resetting the text which causes the
onFocusChanged blur branch to commit dirty state via onValueChange(state.text)
before the reset runs; modify the Escape handling code so you first call
state.setTextAndPlaceCursorAtEnd(currentText) (reset the text to currentText)
and only then call focusManager.clearFocus(), ensuring the blur callback sees
state.text == currentText and does not trigger onValueChange; reference
ConfirmTextField, state.setTextAndPlaceCursorAtEnd(...),
focusManager.clearFocus(), onValueChange(...) and the onFocusChanged blur block
when making the change.

---

Outside diff comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/jewel/textfield/ConfirmTextField.kt`:
- Around line 67-74: The Enter key handler in ConfirmTextField (Key.Enter
branch) calls focusManager.clearFocus() before invoking onValueChange, causing
the blur callback (which compares newText against currentText from
rememberUpdatedState) to also call onValueChange and double-invoke it; add a
commit-in-progress boolean flag (e.g., isCommitting) stored in the component
scope, set it true immediately before calling focusManager.clearFocus() in the
Key.Enter path and in the checkmark button's onClick, then have the blur
callback check this flag and skip its onValueChange commit when isCommitting is
true (reset the flag after committing) so explicit-commit paths do not trigger
the blur commit.

---

Duplicate comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/existingpack/ExistingPackViewModel.kt`:
- Around line 115-127: The call to IconPackWriter.savePack in
viewModelScope.launch relies on the default writeToFile behavior; make the
parameter explicit to match other callers — update the savePack(...) invocation
in ExistingPackViewModel (the call that passes inMemorySettings and
inputFieldState) to include writeToFile = true so the intent is clear and
consistent with NewPackViewModel's behavior.

---

Nitpick comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/material/MaterialPackViewModel.kt`:
- Around line 102-108: saveDestination() in MaterialPackViewModel updates
iconPackDestination but doesn't reset inMemorySettings.flatPackage like
NewPackViewModel.saveDestination() does, leaving stale values between
saveDestination() and saveIconPack(); update
MaterialPackViewModel.saveDestination() to explicitly set
inMemorySettings.flatPackage = false (or document deliberate omission) so the
saved destination state matches NewPackViewModel behavior and avoids reading
stale flatPackage before saveIconPack(), referencing saveDestination(),
saveIconPack(), inMemorySettings.flatPackage, and class MaterialPackViewModel
when making the change.
- Around line 90-100: Extract the duplicate logic from
MaterialPackViewModel.updateDestinationPath and
NewPackViewModel.updateDestinationPath into a shared helper (either a
DirectoryState companion factory or a new PackDestinationHelper function) that
takes a Path and returns the ChooseDestinationDirectoryState/DirectoryState
initialized with iconPackDestination, predictedPackage (using
PackageExtractor.getFrom(...).orEmpty()), and nextAvailable = true; then replace
both MaterialPackViewModel.updateDestinationPath and
NewPackViewModel.updateDestinationPath to call that shared utility and update
_state with its result.
- Around line 42-43: The _events Channel is currently unbuffered which can drop
or suspend sends like _events.send(FinishSetup) if the UI collector is briefly
inactive; change its creation from Channel<MaterialPackEvent>() to a buffered
channel (e.g., Channel(Channel.BUFFERED) or Channel(capacity)) so events are
queued for short-lived suspensions, keeping the existing events =
_events.receiveAsFlow() and MaterialPackEvent/FinishSetup logic intact.
- Around line 116-131: In saveIconPack (MaterialPackViewModel) add a brief
inline comment next to the IconPackWriter.savePack call explaining that
writeToFile = false is intentional because Material packs use icons from an
external Material Design library dependency (no icon files are generated on
disk) and this view model only updates inMemorySettings/configuration rather
than writing icon files; reference the saveIconPack method and the writeToFile =
false flag so future maintainers understand the difference from
NewPackViewModel/ExistingPackViewModel.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/mode/iconpack/newpack/NewPackInputHandler.kt`:
- Around line 9-34: The invalidateInputFieldState implementation in
NewPackInputHandler duplicates the same "preserve user's packageName, fill from
destination if empty" logic found in
MaterialInputHandler.invalidateInputFieldState; extract that shared logic into a
small utility (e.g., a function named resolvePackageNameText or
resolvePackageNameState) and call it from both
NewPackInputHandler.invalidateInputFieldState and
MaterialInputHandler.invalidateInputFieldState so they stay in sync without
duplicated code; update the companion object usage if needed to reuse
PackageExtractor.getFrom(iconPackDestination) via the new utility and keep the
handlers focused on their specific state differences.

@egorikftp egorikftp force-pushed the feature/material-pack branch from f5a5721 to 1907270 Compare February 19, 2026 15:55
@egorikftp egorikftp merged commit faada99 into main Feb 19, 2026
3 checks passed
@egorikftp egorikftp deleted the feature/material-pack branch February 19, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant