diff --git a/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md b/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md
index 7644a1d..509d92c 100644
--- a/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md
+++ b/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md
@@ -10189,7 +10189,7 @@ Summary of what was implemented:
_**User**_
-Sorry, I was referring to "run_step_x" defs in migrator.py.
+"run_step_x" defs in migrator.py should be refactored to match their bl_labels.
---
@@ -10199,3 +10199,1507 @@ _**Agent (model default, mode Agent)**_
---
+Thought Process
+
+The user wants me to refactor the "run_step_x" function names in migrator.py to match their bl_labels. From the conversation summary, the bl_labels for the migrator step operators are:
+- CopyAttr
+- MigNLA
+- MigCustProps
+- MigBoneConst
+- RetargRelatives
+- MigBBodyShapeKeys
+
+So the run_step_1, run_step_2, etc. should be renamed to something like:
+- run_step_1 → run_copy_attributes (or run_copy_attr)
+- run_step_2 → run_migrate_nla (or run_mig_nla)
+- run_step_3 → run_custom_properties (or run_mig_cust_props)
+- run_step_4 → run_bone_constraints (or run_mig_bone_const)
+- run_step_5 → run_retarget_relations (or run_retarg_relatives)
+- run_step_6 → run_basebody_shapekeys (or run_mig_bbody_shapekeys)
+- run_step_7 → (if there is one for remove duplicate constraints)
+
+Let me read migrator.py to see the current function names and how they're used, and also check operators.py for the exact bl_labels.
+
+
+---
+
+
+Checking migrator and operator names so we can rename `run_step_x` to match the operators' `bl_label`s.
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "run_step_|bl_label",
+ "path": "T:\\Dynamic-Link-Manager",
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "run_step_|bl_label",
+ "path": "T:\\Dynamic-Link-Manager",
+ "outputMode": "content",
+ "totalFiles": 9,
+ "totalMatches": 347,
+ "topFiles": [
+ {
+ "uri": "ui\\panels.py",
+ "matchCount": 1
+ },
+ {
+ "uri": "ui/operators.py",
+ "matchCount": 42
+ },
+ {
+ "uri": "ui/panels.py",
+ "matchCount": 1
+ },
+ {
+ "uri": "ui\\operators.py",
+ "matchCount": 42
+ },
+ {
+ "uri": "ops/migrator.py",
+ "matchCount": 12
+ },
+ {
+ "uri": ".specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md",
+ "matchCount": 91
+ },
+ {
+ "uri": ".specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md",
+ "matchCount": 55
+ },
+ {
+ "uri": ".specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md",
+ "matchCount": 91
+ },
+ {
+ "uri": "ops\\migrator.py",
+ "matchCount": 12
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+I have the mapping:
+- run_step_1 → CopyAttr → run_copy_attr
+- run_step_2 → MigNLA → run_mig_nla
+- run_step_3 → MigCustProps → run_mig_cust_props
+- run_step_4 → MigBoneConst → run_mig_bone_const
+- run_step_5 → RetargRelatives → run_retarg_relatives
+- run_step_6 → MigBBodyShapeKeys → run_mig_bbody_shapekeys
+
+I'll use snake_case versions of the bl_labels for the function names to keep them concise and match the labels:
+- run_copy_attr
+- run_mig_nla
+- run_mig_cust_props
+- run_mig_bone_const
+- run_retarg_relatives
+- run_mig_bbody_shapekeys
+
+Now I need to:
+1. Rename the functions in migrator.py
+2. Update the internal calls in run_full_migration in migrator.py
+3. Update the imports and calls in ui/operators.py
+
+
+---
+
+
+Renaming `run_step_1`–`run_step_6` to names that match the bl_labels and updating all call sites.
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+
+Now I need to add a docstring for run_mig_cust_props (run_step_3 had one - let me check). I only replaced "def run_step_3" with "def run_mig_cust_props" so the docstring might still say something generic. Let me check - actually the search only matched the def line, so the next line would still be the docstring. Let me also update ui/operators.py with the new import names and function calls.
+
+
+---
+
+
+Updating operators to use the new function names:
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+Thought Process
+
+Adding a short docstring for run_mig_cust_props to match the others (MigCustProps). Let me check migrator.py - run_step_3 had a docstring; I only changed the def line so the docstring is probably still there. Let me read that section to see if it needs a (MigCustProps) suffix.
+
+
+---
+
+
+Adding a (MigCustProps) suffix to the run_mig_cust_props docstring for consistency.
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+Docstrings look fine. I'll add the (MigCustProps) to run_mig_cust_props and (MigBoneConst) to run_mig_bone_const and (RetargRelatives) to run_retarg_relatives for consistency with the first two. Actually the user only asked to refactor names to match bl_labels - the docstrings are optional. The refactor is complete. Let me verify there are no remaining references to run_step_ in the codebase.
+
+
+---
+
+
+Checking for remaining references to the old names:
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "run_step_[0-9]",
+ "path": "T:\\Dynamic-Link-Manager",
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "run_step_[0-9]",
+ "path": "T:\\Dynamic-Link-Manager",
+ "outputMode": "content",
+ "totalFiles": 2,
+ "totalMatches": 90,
+ "topFiles": [
+ {
+ "uri": ".specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md",
+ "matchCount": 45
+ },
+ {
+ "uri": ".specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md",
+ "matchCount": 45
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+
+
+Tool use: **read_lints**
+
+
+Parameters:
+
+```json
+{
+ "paths": [
+ "T:\\Dynamic-Link-Manager\\ops\\migrator.py",
+ "T:\\Dynamic-Link-Manager\\ui\\operators.py"
+ ]
+}
+```
+Result:
+
+```json
+{
+ "linterErrorsByFile": [
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ },
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\operators.py"
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+No remaining references in the actual Python code (only in .specstory history). Lints are clean. Refactor is complete.
+
+
+---
+
+
+Summary of changes:
+
+**`ops/migrator.py`** – step functions renamed to match operator `bl_label`s:
+
+| Old | New | bl_label |
+|---------------|------------------------|-------------------|
+| `run_step_1` | `run_copy_attr` | CopyAttr |
+| `run_step_2` | `run_mig_nla` | MigNLA |
+| `run_step_3` | `run_mig_cust_props` | MigCustProps |
+| `run_step_4` | `run_mig_bone_const` | MigBoneConst |
+| `run_step_5` | `run_retarg_relatives` | RetargRelatives |
+| `run_step_6` | `run_mig_bbody_shapekeys` | MigBBodyShapeKeys |
+
+- Docstrings for `run_copy_attr` and `run_mig_nla` were updated to mention the bl_label in parentheses; the others keep their existing descriptions.
+- `run_full_migration()` now calls these new names.
+
+**`ui/operators.py`** – each migrator step operator’s `execute()` now imports and calls the corresponding new function (e.g. `run_copy_attr`, `run_mig_nla`, …).
+
+The only remaining `run_step_*` mentions are in `.specstory/history`; no code references the old names and there are no linter errors.
+
+---
+
+_**User**_
+
+MigNLA does not mirror the OrigChar's action slot to the RepChar's action slot when there are no NLA layers or strips. It should also report to the user when "No NLA detected, active action and slot copied to Replacement Armature."
+
+It should also report these messages for each use case:
+"NLA layers detected, Animation Layer attributes migrated to Replacement Armature"
+"NLA layers detected and migrated. No Animation Layers found."
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+Thought Process
+
+The user wants me to create a plan for improving the MigNLA step in the character migrator. Let me understand the requirements:
+
+1. **Action slot when no NLA**: When the original character has no NLA layers or strips, MigNLA should mirror the OrigChar's action slot to the RepChar's action slot. The user says it does not currently do this - so I need to verify the current behavior and ensure it does copy the action slot.
+
+2. **Report when no NLA**: When there are no NLA layers/strips, report: "No NLA detected, active action and slot copied to Replacement Armature."
+
+3. **Report for NLA cases**:
+ - When NLA layers are detected and animation layer attributes are migrated: "NLA layers detected, Animation Layer attributes migrated to Replacement Armature"
+ - When NLA layers are detected and migrated but no Animation Layers found: "NLA layers detected and migrated. No Animation Layers found."
+
+I need to read the current `run_mig_nla` implementation to understand:
+- Does it already copy the active action to rep when there's no NLA?
+- What are "Animation Layer attributes" - likely `als.turn_on` and related?
+- How to add reporting - the migrator functions don't have access to `self.report()`; they're called from operators that do. So we need to either pass a report callback or have the operator do the reporting based on return value from the migrator.
+
+Let me search the codebase to understand the current flow and how reporting works.
+
+
+---
+
+
+Checking how MigNLA works and how operators report messages.
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "run_mig_nla|MigNLA|self\\.report",
+ "path": "T:\\Dynamic-Link-Manager\\ui\\operators.py",
+ "contextBefore": 0,
+ "contextAfter": 2,
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "run_mig_nla|MigNLA|self\\.report",
+ "path": "T:\\Dynamic-Link-Manager\\ui\\operators.py",
+ "outputMode": "content",
+ "totalFiles": 1,
+ "totalMatches": 65,
+ "topFiles": [
+ {
+ "uri": "ui\\operators.py",
+ "matchCount": 181
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+Now I understand:
+
+1. **Current behavior when no NLA**:
+ - If `not orig.animation_data` → return (no copy, no report).
+ - If `has_nla` is False and `active_action` exists → copy action to rep, call _mirror_als_turn_on, return. So it *does* copy the action slot when there are no NLA tracks but there is an active action.
+ - If `has_nla` is False and no active_action → return (nothing copied). So when there are no NLA layers AND no active action, we don't copy anything - and we don't report. The user says "MigNLA does not mirror the OrigChar's action slot to the RepChar's action slot when there are no NLA layers or strips." So maybe the issue is: (a) when there's no active action we should still ensure rep's slot is cleared? Or (b) the copy isn't working in some case? Or (c) when there ARE no NLA layers we should always set rep.animation_data.action to match orig (even if None)? I'll assume the user wants: when no NLA, always mirror the action slot (set rep.animation_data.action = orig.animation_data.action, which may be None), and report "No NLA detected, active action and slot copied to Replacement Armature." when we do that. If there's no active action, the message still makes sense: we "copied" the slot (which is empty).
+
+2. **Reporting**: The migrator `run_mig_nla` doesn't have access to `self.report`. So we need to either:
+ - Have `run_mig_nla` return a string (message) and the operator calls `self.report({"INFO"}, msg)`, or
+ - Pass a report callback into `run_mig_nla`, e.g. `run_mig_nla(orig, rep, report=None)` and if report, call it with the message.
+
+ The cleanest approach that matches other code (e.g. library.scan_linked_assets(context, self.report)) is to pass an optional `report` callback. Then the operator would do: `run_mig_nla(orig, rep, report=self.report)` and the migrator would call `report({"INFO"}, msg)` when appropriate. But that would require the operator to pass a callable that matches Blender's report signature `(type, message)`. So in migrator we'd do `if report: report({"INFO"}, msg)`.
+
+3. **Use cases and messages**:
+ - No NLA: "No NLA detected, active action and slot copied to Replacement Armature." (and ensure we actually set rep's action slot from orig, including when orig has no action)
+ - NLA detected + Animation Layer attributes migrated: "NLA layers detected, Animation Layer attributes migrated to Replacement Armature." (Animation Layer attributes = als.turn_on mirroring; we need to detect if _mirror_als_turn_on actually did something, or we could just say this whenever we have NLA and we call _mirror_als_turn_on - the message is about the fact that we migrated them, not whether orig had any)
+ - NLA detected + no Animation Layers: "NLA layers detected and migrated. No Animation Layers found."
+
+ So we need to know if there were any "animation layer" attributes (als.turn_on on armature or any bone). We could have _mirror_als_turn_on return True if it found any, or we could check orig for presence of als.turn_on before mirroring. I'll define "Animation Layers found" as: orig armature or any pose bone has "als.turn_on". Then after migrating NLA we report one of the two NLA messages.
+
+Let me also double-check: when there are no NLA tracks but there IS an active action, we already set `rep.animation_data.action = active_action`. So we do copy. The user said "does not mirror" - perhaps the bug is when there are no NLA tracks AND no active action? In that case we currently return without creating animation_data on rep or setting action to None. So rep might keep an old action. So "mirror the action slot" should mean: when no NLA, set rep.animation_data.action = orig.animation_data.action (creating animation_data on rep if needed). That way we mirror "empty" too. And report the message.
+
+Plan:
+
+1. **ops/migrator.py `run_mig_nla`**
+ - When `not orig.animation_data`: return (no change; could report "No animation data on original" but user didn't ask for that).
+ - When no NLA (`not has_nla`):
+ - Ensure rep has animation_data (create if None).
+ - Set `rep.animation_data.action = active_action` (already do when active_action; when active_action is None, set it to None to mirror empty slot).
+ - Call _mirror_als_turn_on.
+ - Return a sentinel or call report with "No NLA detected, active action and slot copied to Replacement Armature."
+ - When NLA exists:
+ - After migrating tracks and _mirror_als_turn_on, determine if "Animation Layer attributes" were present: e.g. check if orig (armature) or any orig.pose.bones has "als.turn_on".
+ - Return or report accordingly: "NLA layers detected, Animation Layer attributes migrated to Replacement Armature." vs "NLA layers detected and migrated. No Animation Layers found."
+
+2. **Reporting mechanism**: Have `run_mig_nla(orig, rep, report=None)`. If `report` is not None, call `report({"INFO"}, msg)` with the appropriate message. Operator passes `self.report`.
+
+3. **ui/operators.py** MigNLA operator: pass `self.report` into `run_mig_nla(orig, rep, report=self.report)`. Remove or keep the generic "Migrate NLA done." - we can remove it since we'll have specific messages, or keep it as fallback if no message was reported. Better: only report the specific message from migrator (so migrator always reports one message for the case that ran), and operator doesn't need to report "done" if migrator reported. So operator could do: run_mig_nla(orig, rep, report=self.report); return FINISHED (and migrator reports the right message). If we don't pass report, migrator could still work without reporting (e.g. when called from run_full_migration). So when run_full_migration runs, it invokes the 6 operators - each operator will call run_mig_nla with self.report, so the user will see the message. Good.
+
+ When "Run Character Migration" runs, it invokes each step as an operator (bpy.ops.dlm...). So each operator's execute runs and will pass self.report. So we're good.
+
+4. **Edge case**: orig has animation_data but no NLA tracks and no active action. Then we should create rep.animation_data if needed, set rep.animation_data.action = None, _mirror_als_turn_on (no-op likely), report "No NLA detected, active action and slot copied to Replacement Armature."
+
+I'll draft the plan.
+
+
+---
+
+
+Checking how the operator invokes the migrator and whether run_full_migration calls the operator or the function:
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+So when "Run Character Migration" is used, it calls `bpy.ops.dlm.migrator_migrate_nla()` which runs the MigNLA operator, which has access to self.report. So if we pass self.report into run_mig_nla from the operator, the message will show when running either the single MigNLA button or the full migration. When run_full_migration is called directly (e.g. from script), it calls run_mig_nla(orig, rep) without report - so no message in that path. That's fine.
+
+Now: "Animation Layers" - the user's two NLA messages are:
+- "NLA layers detected, Animation Layer attributes migrated to Replacement Armature."
+- "NLA layers detected and migrated. No Animation Layers found."
+
+So "Animation Layer attributes" = the als.turn_on custom props that we mirror with _mirror_als_turn_on. We need to detect if the original had any of these (on armature or any bone). So before or after _mirror_als_turn_on we check: has_als = (key in orig) or any(key in pb for pb in orig.pose.bones). Then report the first message if has_als, else the second.
+
+I'll create the plan.
+
+
+---
+
+
+Checking the no-NLA path when the original has no action:
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+So when `not has_nla` and `not active_action`, we hit line 79-80 and return without:
+- Creating rep.animation_data
+- Setting rep.animation_data.action = None (to mirror empty slot)
+- Reporting
+
+So the fix is:
+1. When not has_nla: always ensure rep has animation_data, set rep.animation_data.action = active_action (which may be None), call _mirror_als_turn_on, then report and return.
+2. Add optional report callback to run_mig_nla.
+3. For NLA path: after migrating, check if orig had any als.turn_on; report the appropriate message.
+
+Creating the plan.
+
+
+---
+
+
+Creating the plan.
+
+
+---
+
+
+
+Tool use: **create_plan**
+
+
+Parameters:
+
+```json
+{
+ "plan": "# MigNLA: mirror action slot when no NLA and add outcome reports\n\n## Current behavior (gaps)\n\n- **No NLA, no action:** [ops/migrator.py](T:\\Dynamic-Link-Manager\\ops\\migrator.py) returns at 79–80 without creating `rep.animation_data` or setting `rep.animation_data.action`, so the replacement’s action slot is not mirrored (and nothing is reported).\n- **No NLA, with action:** Action is copied and `_mirror_als_turn_on` runs, but no specific report.\n- **NLA path:** Tracks/strips and `_mirror_als_turn_on` run, but the operator only reports a generic \"Migrate NLA done.\"\n\n## 1. Mirror action slot when no NLA (all cases)\n\nIn `run_mig_nla`, when `not has_nla`:\n\n- Ensure replacement has `animation_data` (create if missing).\n- Set `rep.animation_data.action = active_action` in all cases (including when `active_action` is `None`, so an empty slot is mirrored).\n- Call `_mirror_als_turn_on(orig, rep)` (already done when there was an action).\n- Then report and return (see reporting below).\n\nSo the single branch should be: `if not has_nla:` then create rep `animation_data` if needed, set `rep.animation_data.action = active_action`, `_mirror_als_turn_on(orig, rep)`, report, return.\n\n## 2. Reporting from the migrator\n\n- Add an optional parameter to `run_mig_nla`: `report=None`. Signature: `run_mig_nla(orig, rep, report=None)`.\n- When `report` is not None, call `report({\"INFO\"}, msg)` with the correct message for the path taken. Use Blender’s report signature: `(type, message)`.\n\n**Messages:**\n\n| Case | Message |\n|------|--------|\n| No NLA (action/slot copied) | `\"No NLA detected, active action and slot copied to Replacement Armature.\"` |\n| NLA present, Animation Layer attributes found | `\"NLA layers detected, Animation Layer attributes migrated to Replacement Armature.\"` |\n| NLA present, no Animation Layers | `\"NLA layers detected and migrated. No Animation Layers found.\"` |\n\n“Animation Layer attributes” = presence of `als.turn_on` on the original (armature or any pose bone). After migrating NLA and calling `_mirror_als_turn_on`, check: `key = \"als.turn_on\"` present on `orig` or on any `orig.pose.bones`. If yes, use the “migrated” message; otherwise use “No Animation Layers found.”\n\n## 3. Operator wiring\n\n- In [ui/operators.py](T:\\Dynamic-Link-Manager\\ui\\operators.py), MigNLA operator’s `execute`: call `run_mig_nla(orig, rep, report=self.report)`.\n- Remove the generic `self.report({\"INFO\"}, \"Migrate NLA done.\")` so the message comes only from `run_mig_nla` (no duplicate).\n\n## 4. Call sites\n\n- **MigNLA button:** Uses operator → `run_mig_nla(..., report=self.report)` → user sees the new messages.\n- **Run Character Migration:** Invokes `bpy.ops.dlm.migrator_migrate_nla()`, so the same operator runs and the same report will appear.\n- **run_full_migration** in migrator (if ever called with no report): `run_mig_nla(orig, rep)` remains valid; no report in that case.\n\n## Files to change\n\n- **[ops/migrator.py](T:\\Dynamic-Link-Manager\\ops\\migrator.py):** \n - `run_mig_nla(orig, rep, report=None)`. \n - No-NLA branch: always ensure rep `animation_data`, set `rep.animation_data.action = active_action`, `_mirror_als_turn_on`, then if report: `report({\"INFO\"}, \"No NLA detected, active action and slot copied to Replacement Armature.\")`; return. \n - NLA branch: after copying tracks/strips and `_mirror_als_turn_on`, set `has_als = (\"als.turn_on\" in orig) or any(\"als.turn_on\" in pb for pb in orig.pose.bones)`; if report: `report({\"INFO\"}, \"NLA layers detected, Animation Layer attributes migrated to Replacement Armature.\")` if has_als else `report({\"INFO\"}, \"NLA layers detected and migrated. No Animation Layers found.\")`.\n- **[ui/operators.py](T:\\Dynamic-Link-Manager\\ui\\operators.py):** MigNLA operator: pass `report=self.report` into `run_mig_nla`, remove the standalone `self.report({\"INFO\"}, \"Migrate NLA done.\")`.\n",
+ "name": "MigNLA action slot and reports",
+ "overview": "Fix MigNLA so the action slot is always mirrored when there are no NLA layers/strips (including when the slot is empty), and add user-visible reports for all three outcomes: no NLA (action/slot copied), NLA migrated with Animation Layer attributes, and NLA migrated without Animation Layers."
+}
+```
+Additional data:
+
+```json
+{
+ "reviewData": {
+ "status": "Requested",
+ "selectedOption": "none",
+ "isShowingInput": false
+ },
+ "planUri": "file:///c%3A/Users/Nathan/.cursor/plans/mignla_action_slot_and_reports_125f4795.plan.md",
+ "hasOpenedEditor": true
+}
+```
+Result:
+
+```json
+{
+ "rejected": {}
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+_**User**_
+
+Implement the plan as specified, it is attached for your reference. Do NOT edit the plan file itself.
+
+To-do's from the plan have already been created. Do not create them again. Mark them as in_progress as you work, starting with the first one. Don't stop until you have completed all the to-dos.
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+Thought Process
+
+The user wants me to implement the plan for MigNLA. Let me read the plan again and the current code, then implement.
+
+Plan summary:
+1. In ops/migrator.py - run_mig_nla(orig, rep, report=None):
+ - No-NLA branch: always ensure rep animation_data, set rep.animation_data.action = active_action, _mirror_als_turn_on, then if report: report the no-NLA message; return.
+ - NLA branch: after copying tracks/strips and _mirror_als_turn_on, check has_als and if report: report the appropriate NLA message.
+2. In ui/operators.py: pass report=self.report to run_mig_nla, remove "Migrate NLA done."
+
+The plan said "To-do's from the plan have already been created" - the plan file shows `todos: []` so there may be no todos in the file, or they're elsewhere. I'll implement without relying on todo IDs and mark progress if I find todos.
+
+Let me read the current migrator run_mig_nla and operators MigNLA section to make the edits.
+
+
+---
+
+
+Implementing the MigNLA plan: reading the current code and applying the changes.
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+---
+
+Thought Process
+
+Now I'll implement:
+
+1. migrator.py: Refactor run_mig_nla to:
+ - Add report=None parameter
+ - No-NLA: single branch that always creates rep.animation_data if needed, sets rep.animation_data.action = active_action, _mirror_als_turn_on, then if report: report no-NLA message; return
+ - NLA: after _mirror_als_turn_on, compute has_als and if report: report the appropriate message
+
+2. operators.py: run_mig_nla(orig, rep, report=self.report), remove the "Migrate NLA done." line
+
+
+---
+
+
+Implementing changes in both files.
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+Thought Process
+
+Both edits are done. Let me verify there are no other call sites that need the report parameter (run_full_migration calls run_mig_nla(orig, rep) with two args - that's fine, report stays None). Check lints.
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **read_lints**
+
+
+Parameters:
+
+```json
+{
+ "paths": [
+ "T:\\Dynamic-Link-Manager\\ops\\migrator.py",
+ "T:\\Dynamic-Link-Manager\\ui\\operators.py"
+ ]
+}
+```
+Result:
+
+```json
+{
+ "linterErrorsByFile": [
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ },
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\operators.py"
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+Implementation is complete. The plan had no todos in the snippet (todos: []), so there's nothing to mark. Summary for the user.
+
+
+---
+
+
+Summary of what was implemented:
+
+**1. [ops/migrator.py](T:\Dynamic-Link-Manager\ops\migrator.py)**
+
+- **Signature:** `run_mig_nla(orig, rep, report=None)`.
+- **No-NLA path:** Single branch that always:
+ - Creates `rep.animation_data` if missing,
+ - Sets `rep.animation_data.action = active_action` (including `None` to mirror an empty slot),
+ - Calls `_mirror_als_turn_on(orig, rep)`,
+ - If `report` is set: `report({"INFO"}, "No NLA detected, active action and slot copied to Replacement Armature.")`.
+- **NLA path:** After copying tracks/strips and calling `_mirror_als_turn_on`, if `report` is set it reports:
+ - `"NLA layers detected, Animation Layer attributes migrated to Replacement Armature."` when `als.turn_on` exists on the original armature or any pose bone,
+ - `"NLA layers detected and migrated. No Animation Layers found."` otherwise.
+
+**2. [ui/operators.py](T:\Dynamic-Link-Manager\ui\operators.py)**
+
+- MigNLA operator calls `run_mig_nla(orig, rep, report=self.report)`.
+- The generic `self.report({"INFO"}, "Migrate NLA done.")` was removed so only the migrator’s messages are shown.
+
+`run_full_migration` still calls `run_mig_nla(orig, rep)` with two arguments, so `report` stays `None` and no report is used when running the full migration via that path; when MigNLA is run from the UI (or via the Run Character Migration operator, which invokes the MigNLA operator), the user sees the new INFO messages. No linter issues reported.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt the slot was not applied.
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+
+
+---
+
+Thought Process
+
+The user is referring to something about "the slot was not applied" and has attached the terminals file. Let me read the terminal output to understand what they're referring to - likely the shape key action slot not being applied to the replacement character's base body during the migration workflow.
+
+
+---
+
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt
+
+
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "shapekey|shape_key|action.*slot|key_blocks",
+ "path": "T:\\Dynamic-Link-Manager\\ops",
+ "caseInsensitive": true
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "shapekey|shape_key|action.*slot|key_blocks",
+ "path": "T:\\Dynamic-Link-Manager\\ops",
+ "outputMode": "content",
+ "totalFiles": 2,
+ "totalMatches": 34,
+ "topFiles": [
+ {
+ "uri": "ops/migrator.py",
+ "matchCount": 17
+ },
+ {
+ "uri": "ops\\migrator.py",
+ "matchCount": 17
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+The user says "the slot was not applied." From the terminal I see:
+- Line 363: "No NLA detected, active action and slot copied to Replacement Armature."
+So MigNLA reports that it copied the active action and slot when there's no NLA. But the user says the slot was not applied.
+
+So the issue is: when we have no NLA and only an active action, we're supposed to apply that action to the replacement armature. The "slot" likely refers to the shape-key action slot on the base body (step 6), or possibly the armature's action slot.
+
+Looking at the conversation summary again:
+- "add the CC_Base_Body.002Action to Eddy.001's shape key action slot" - so "slot" often means the shape key action slot on the mesh.
+- Step 6 does: override base body, copy shape key values, then assign the shape-key action to the replacement's shape_keys.animation_data.action.
+
+So "the slot was not applied" could mean:
+1. The shape-key action was not assigned to the replacement's base body (the Shape Key Editor "Action" slot).
+2. Or the armature's active action slot wasn't applied when there's no NLA.
+
+Given the terminal shows "No NLA detected, active action and slot copied to Replacement Armature" and then the user says "the slot was not applied", they might be saying that despite that message, the slot (armature action or shape key action) didn't actually get applied.
+
+Let me look at the migrator code for:
+1. Where we set rep.animation_data.action when there's no NLA
+2. Where we set the shape key action in step 6
+
+I need to read the full run_mig_nla (no-NLA path) and run_mig_bbody_shapekeys (shape key action assignment) to see what "slot" might not be applied.
+
+
+---
+
+
+Checking how the action slot is applied in the no-NLA path and in the base-body shape-key step:
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+I see. The "slot" the user means is likely the **shape-key action slot** on the base body. In step 6 we look for the action by:
+- `body_name + "Action"` (e.g. "CC_Base_Body.002Action")
+- `ob.data.name + "Action"`
+- `body_name + "Action.001"`
+
+But the **original** character's base body might have a shape-key action assigned that has a different name (e.g. from the library, or "CC_Base_BodyAction"). We're not copying the action **from the original's shape_keys.animation_data.action** to the replacement—we're only guessing by naming convention. So if the action has a different name (e.g. "CC_Base_Body.002Action" in the scene but we're on replacement body "CC_Base_Body.003"), we do get it. But the key point: we should **use the same action that the original base body uses** for its shape keys, not just try to find one by name.
+
+So the fix: for each replacement base body, find the **original** base body and get its `shape_keys.animation_data.action`; assign that same action to the replacement's shape_keys.animation_data. That way the "slot" (the assignment of which action drives the shape keys) is applied.
+
+Let me update the logic:
+1. If we have `orig_base` and `orig_base.data.shape_keys` and `orig_base.data.shape_keys.animation_data` and `orig_base.data.shape_keys.animation_data.action`, use that action for the replacement.
+2. Else fall back to the current name-based lookup.
+
+That way the slot (the action assigned to the shape key block) is copied from original to replacement.
+
+
+---
+
+
+Checking how the shape-key action is chosen and whether we should use the original base body's assigned action:
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+The issue is clear. We only assign the shape-key action by **name lookup** (body_name + "Action", etc.). We never copy the **slot** from the original base body—i.e. we don't set `rep.data.shape_keys.animation_data.action = orig_base.data.shape_keys.animation_data.action`. So if the original has an action like "CC_Base_Body.002Action" assigned in its Shape Key Editor slot, we might be looking for "CC_Base_Body.003Action" for the replacement (CC_Base_Body.003) and not find it, or we might find a different action. The fix: **prefer the action from the original base body's shape_keys.animation_data** when we have an orig_base; only fall back to name-based lookup when the original has no action in that slot.
+
+
+---
+
+
+Using the original base body's shape-key action when present; applying that action to the replacement's shape-key slot.
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+
+Good. Now the replacement base body will get the same shape-key action as the original's slot when we have an orig_base with a shape key action assigned. If the original has no action in that slot, we still fall back to the name-based lookup. Let me verify there are no linter issues.
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **read_lints**
+
+
+Parameters:
+
+```json
+{
+ "paths": [
+ "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ ]
+}
+```
+Result:
+
+```json
+{
+ "linterErrorsByFile": [
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+Good. The fix is in. Let me summarize for the user.
+
+
+---
+
+
+The “slot” that wasn’t applied was the **shape-key action** on the replacement base body (the action in the Shape Key Editor that drives the keys).
+
+**What was wrong**
+Step 6 was only guessing the action by name (`body_name + "Action"`, etc.). It did **not** use the action actually assigned on the **original** character’s base body. So the replacement could end up with no action or the wrong one.
+
+**What was changed**
+The replacement base body now uses the **same** shape-key action as the original when we have a matching original base body:
+
+1. If the original base body has `shape_keys.animation_data.action` set, that action is assigned to the replacement’s shape-key slot.
+2. Only if the original has no action there do we fall back to the old name-based lookup.
+
+So the shape-key “slot” is now copied from original to replacement. Reload the add-on and run “BaseBody ShapeKeys” (or the full migration) again; the replacement’s shape keys should use the same action as the original.
+
+---
+
+_**User**_
+
+Not the shapekey action, we're looking in MigNLA; when there are no anim layers but there is an active action, it's supposed to apply the action to RepChar. It applies the action, but the Action Slot is not applied.
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+Thought Process
+
+The user is clarifying: the issue is in MigNLA, not step 6. When there are no NLA tracks but there is an active action on the original, we copy the action to the replacement. The user says "it applies the action, but the Action Slot is not applied."
+
+In Blender, the "Action Slot" on an armature could mean:
+1. The `animation_data.action` - we're already setting `rep.animation_data.action = active_action` in the no-NLA path. So the action is applied.
+2. Maybe there's a separate "slot" concept - like the NLA strip slot or the way the action is displayed/used?
+
+Wait - in Blender 4.x/5.x, armatures can have:
+- `animation_data.action` - the active action
+- NLA tracks
+
+Perhaps "Action Slot" means something specific in their workflow - like the action needs to be pushed to an NLA strip (a "slot") rather than just set as the active action? Or there's a different property that controls whether the action is "in a slot"?
+
+Let me search for what "action slot" might mean in Blender's API - perhaps `action_extrapolation`, `action_blend_type`, or the way the action is evaluated. Or maybe they use a specific addon (Animation Layers?) that has a "slot" concept.
+
+Actually re-reading again: "it applies the action, but the Action Slot is not applied" - so the action IS being applied (we set rep.animation_data.action = active_action), but some "Action Slot" is not. So there must be another property related to how the action is used. In Blender:
+- animation_data has: action, nla_tracks, ...
+- When you have an action in the "action slot" in the UI, there might be blend mode, extrapolation for the action layer?
+
+Let me check Blender's animation_data - there might be:
+- action_extrapolation (how the action behaves before/after its range)
+- action_blend_type (for the action strip?)
+
+I'll search the codebase and web for animation_data action slot properties.
+
+
+---
+
+
+Checking Blender's animation_data for an "action slot" or related properties we might not be copying:
+
+
+---
+
+
+
+ Tool use: **web_search** • Searched web `undefined` • **1** results
+
+| Title | URL | Chunk |
+|-------|-----|-------|
+| Web search results for query: Blender animation_data action slot extrapolation blend type Python API | | Links:
1. [AnimData(bpy_struct) - Blender Python API](https://docs.blender.org/api/current/bpy.types.AnimData.html)
2. [ActionSlot(bpy_struct) - Blender Python API](https://docs.blender.org/api/current/bpy.types.ActionSlot.html)
3. [How exactly do ActionSlots work?](https://blender.stackexchange.com/questions/339852/how-exactly-do-actionslots-work)
4. [Action(ID) - Blender Python API](https://docs.blender.org/api/current/bpy.types.Action.html)
5. [Slotted Actions - Blender Developer Documentation](https://developer.blender.org/docs/release_notes/4.4/upgrading/slotted_actions)
# Blender AnimData Action Slot Extrapolation and Blend Type
## Action Slot Properties in AnimData
The `AnimData` class provides several key properties for controlling how the active action interacts with the NLA stack:[1]
**action_slot**: Identifies which subset of the Action animates this data-block. It's a reference to an `ActionSlot` object whose name is used to find the correct slot when assigning an Action.[1]
**action_extrapolation**: Controls behavior for gaps past the Active Action's range when evaluating with NLA. Options include:[1]
- `NOTHING` – Strip has no influence past its extents
- `HOLD` – Hold first frame if no previous strips, always hold last frame
- `HOLD_FORWARD` – Only hold last frame
**action_blend_type**: Specifies how the Active Action's result combines with NLA stack results. Options include:[1]
- `REPLACE` – Replace accumulated results by influence amount
- `COMBINE` – Combine using addition, multiplication, or quaternion math based on channel type
- `ADD` – Add weighted result to accumulated results
- `SUBTRACT` – Remove weighted result from accumulated results
- `MULTIPLY` – Multiply weighted result with accumulated results
## ActionSlot Structure
An `ActionSlot` identifies a set of animation channels within an Action for a specific data-block type.[2] Each slot has a `target_id_type` property specifying what type of data-block it animates (Object, Armature, Material, etc.), and slots can be accessed via `action.slots`.[4]
## Accessing F-Curves via Slots (Blender 4.4+)
Since Blender 4.4, actions use a layered structure. To access F-curves for a specific slot:[3]
```python
strip = action.layers[0].strips[0]
channelbag = strip.channelbag(slot, ensure=True)
fcurves = channelbag.fcurves
``` |
+
+
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "animation_data|\\.action",
+ "path": "T:\\Dynamic-Link-Manager\\ops\\migrator.py",
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "animation_data|\\.action",
+ "path": "T:\\Dynamic-Link-Manager\\ops\\migrator.py",
+ "outputMode": "content",
+ "totalFiles": 1,
+ "totalMatches": 18,
+ "topFiles": [
+ {
+ "uri": "ops\\migrator.py",
+ "matchCount": 18
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+So in Blender 4.4+, there's an **ActionSlot** concept. The search says:
+- `action_slot`: Identifies which subset of the Action animates this data-block. It's a reference to an `ActionSlot` object whose name is used to find the correct slot when assigning an Action.
+- So when we set `rep.animation_data.action = active_action`, we're setting the action, but maybe we also need to set `rep.animation_data.action_slot` to match the original's action_slot?
+
+Let me fetch the Blender API docs for AnimData and ActionSlot to see the exact properties.
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **web_fetch**
+
+
+Parameters:
+
+```json
+{
+ "url": "https://docs.blender.org/api/current/bpy.types.AnimData.html"
+}
+```
+Result:
+
+```json
+{
+ "url": "https://docs.blender.org/api/current/bpy.types.AnimData.html",
+ "markdown": "AnimData(bpy_struct) - Blender Python API\n\n# AnimData(bpy_struct)¶\n\nbase class — [bpy_struct](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct)\n\nclass bpy.types.AnimData(bpy_struct)¶\n\nAnimation data for data-block\n\naction¶\n\nActive Action for this data-block\n\nType:\n\n[Action](https://docs.blender.org/api/current/bpy.types.Action.html#bpy.types.Action)\n\naction_blend_type¶\n\nMethod used for combining Active Action’s result with result of NLA stack\n\n`REPLACE` Replace – The strip values replace the accumulated results by amount specified by influence.\n\n`COMBINE` Combine – The strip values are combined with accumulated results by appropriately using addition, multiplication, or quaternion math, based on channel type.\n\n`ADD` Add – Weighted result of strip is added to the accumulated results.\n\n`SUBTRACT` Subtract – Weighted result of strip is removed from the accumulated results.\n\n`MULTIPLY` Multiply – Weighted result of strip is multiplied with the accumulated results.\n\nType:\n\nenum in [`'REPLACE'`,`'COMBINE'`,`'ADD'`,`'SUBTRACT'`,`'MULTIPLY'`], default`'REPLACE'`\n\naction_extrapolation¶\n\nAction to take for gaps past the Active Action’s range (when evaluating with NLA)\n\n`NOTHING` Nothing – Strip has no influence past its extents.\n\n`HOLD` Hold – Hold the first frame if no previous strips in track, and always hold last frame.\n\n`HOLD_FORWARD` Hold Forward – Only hold last frame.\n\nType:\n\nenum in [`'NOTHING'`,`'HOLD'`,`'HOLD_FORWARD'`], default`'HOLD'`\n\naction_influence¶\n\nAmount the Active Action contributes to the result of the NLA stack\n\nType:\n\nfloat in [0, 1], default 1.0\n\naction_slot¶\n\nThe slot identifies which sub-set of the Action is considered to be for this data-block, and its name is used to find the right slot when assigning an Action\n\nType:\n\n[ActionSlot](https://docs.blender.org/api/current/bpy.types.ActionSlot.html#bpy.types.ActionSlot)\n\naction_slot_handle¶\n\nA number that identifies which sub-set of the Action is considered to be for this data-block\n\nType:\n\nint in [-inf, inf], default 0\n\naction_slot_handle_tweak_storage¶\n\nStorage to temporarily hold the main action slot while in tweak mode\n\nType:\n\nint in [-inf, inf], default 0\n\naction_suitable_slots¶\n\nThe list of slots in this animation data-block\n\nType:\n\n[bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of [ActionSlot](https://docs.blender.org/api/current/bpy.types.ActionSlot.html#bpy.types.ActionSlot), (readonly)\n\naction_tweak_storage¶\n\nStorage to temporarily hold the main action while in tweak mode\n\nType:\n\n[Action](https://docs.blender.org/api/current/bpy.types.Action.html#bpy.types.Action)\n\ndrivers¶\n\nThe Drivers/Expressions for this data-block\n\nType:\n\n[AnimDataDrivers](https://docs.blender.org/api/current/bpy.types.AnimDataDrivers.html#bpy.types.AnimDataDrivers) [bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of [FCurve](https://docs.blender.org/api/current/bpy.types.FCurve.html#bpy.types.FCurve), (readonly)\n\nlast_slot_identifier¶\n\nThe identifier of the most recently assigned action slot. The slot identifies which sub-set of the Action is considered to be for this data-block, and its identifier is used to find the right slot when assigning an Action.\n\nType:\n\nstring, default “”, (never None)\n\nnla_tracks¶\n\nNLA Tracks (i.e. Animation Layers)\n\nType:\n\n[NlaTracks](https://docs.blender.org/api/current/bpy.types.NlaTracks.html#bpy.types.NlaTracks) [bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of [NlaTrack](https://docs.blender.org/api/current/bpy.types.NlaTrack.html#bpy.types.NlaTrack), (readonly)\n\nuse_nla¶\n\nNLA stack is evaluated when evaluating this block\n\nType:\n\nboolean, default False\n\nuse_pin¶ Type:\n\nboolean, default False\n\nuse_tweak_mode¶\n\nWhether to enable or disable tweak mode in NLA\n\nType:\n\nboolean, default False\n\nnla_tweak_strip_time_to_scene(frame, *, invert=False)¶\n\nConvert a time value from the local time of the tweaked strip to scene time, exactly as done by built-in key editing tools. Returns the input time unchanged if not tweaking.\n\nParameters:\n\nframe (float in [-1.04857e+06, 1.04857e+06]) – Input time\n\ninvert (boolean, (optional)) – Invert, Convert scene time to action time\n\nReturns:\n\nConverted time\n\nReturn type:\n\nfloat in [-1.04857e+06, 1.04857e+06]\n\nfix_paths_rename_all(*, prefix='', old_name='', new_name='')¶\n\nRename the property paths in the animation system, since properties are animated via string paths, it’s needed to keep them valid after properties has been renamed\n\nParameters:\n\nprefix (string, (optional, never None)) – Prefix, Name prefix\n\nold_name (string, (optional, never None)) – Old Name, Old name\n\nnew_name (string, (optional, never None)) – New Name, New name\n\nclassmethod bl_rna_get_subclass(id, default=None, /)¶ Parameters:\n\nid (str) – The RNA type identifier.\n\nReturns:\n\nThe RNA type or default when not found.\n\nReturn type:\n\n[bpy.types.Struct](https://docs.blender.org/api/current/bpy.types.Struct.html#bpy.types.Struct) subclass\n\nclassmethod bl_rna_get_subclass_py(id, default=None, /)¶ Parameters:\n\nid (str) – The RNA type identifier.\n\nReturns:\n\nThe class or default when not found.\n\nReturn type:\n\ntype\n\n## Inherited Properties¶\n\n[bpy_struct.id_data](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_data)\n\n## Inherited Functions¶\n\n[bpy_struct.as_pointer](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.as_pointer)\n\n[bpy_struct.driver_add](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.driver_add)\n\n[bpy_struct.driver_remove](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.driver_remove)\n\n[bpy_struct.get](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.get)\n\n[bpy_struct.id_properties_clear](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_clear)\n\n[bpy_struct.id_properties_ensure](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_ensure)\n\n[bpy_struct.id_properties_ui](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_ui)\n\n[bpy_struct.is_property_hidden](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_hidden)\n\n[bpy_struct.is_property_overridable_library](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_overridable_library)\n\n[bpy_struct.is_property_readonly](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_readonly)\n\n[bpy_struct.is_property_set](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_set)\n\n[bpy_struct.items](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items)\n\n[bpy_struct.keyframe_delete](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keyframe_delete)\n\n[bpy_struct.keyframe_insert](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keyframe_insert)\n\n[bpy_struct.keys](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keys)\n\n[bpy_struct.path_from_id](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_from_id)\n\n[bpy_struct.path_from_module](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_from_module)\n\n[bpy_struct.path_resolve](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_resolve)\n\n[bpy_struct.pop](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.pop)\n\n[bpy_struct.property_overridable_library_set](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.property_overridable_library_set)\n\n[bpy_struct.property_unset](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.property_unset)\n\n[bpy_struct.rna_ancestors](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.rna_ancestors)\n\n[bpy_struct.type_recast](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.type_recast)\n\n[bpy_struct.values](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.values)\n\n## References¶\n\n[Annotation.animation_data](https://docs.blender.org/api/current/bpy.types.Annotation.html#bpy.types.Annotation.animation_data)\n\n[Armature.animation_data](https://docs.blender.org/api/current/bpy.types.Armature.html#bpy.types.Armature.animation_data)\n\n[CacheFile.animation_data](https://docs.blender.org/api/current/bpy.types.CacheFile.html#bpy.types.CacheFile.animation_data)\n\n[Camera.animation_data](https://docs.blender.org/api/current/bpy.types.Camera.html#bpy.types.Camera.animation_data)\n\n[Curve.animation_data](https://docs.blender.org/api/current/bpy.types.Curve.html#bpy.types.Curve.animation_data)\n\n[Curves.animation_data](https://docs.blender.org/api/current/bpy.types.Curves.html#bpy.types.Curves.animation_data)\n\n[FreestyleLineStyle.animation_data](https://docs.blender.org/api/current/bpy.types.FreestyleLineStyle.html#bpy.types.FreestyleLineStyle.animation_data)\n\n[GreasePencil.animation_data](https://docs.blender.org/api/current/bpy.types.GreasePencil.html#bpy.types.GreasePencil.animation_data)\n\n[ID.animation_data_create](https://docs.blender.org/api/current/bpy.types.ID.html#bpy.types.ID.animation_data_create)\n\n[Key.animation_data](https://docs.blender.org/api/current/bpy.types.Key.html#bpy.types.Key.animation_data)\n\n[Lattice.animation_data](https://docs.blender.org/api/current/bpy.types.Lattice.html#bpy.types.Lattice.animation_data)\n\n[Light.animation_data](https://docs.blender.org/api/current/bpy.types.Light.html#bpy.types.Light.animation_data)\n\n[LightProbe.animation_data](https://docs.blender.org/api/current/bpy.types.LightProbe.html#bpy.types.LightProbe.animation_data)\n\n[Mask.animation_data](https://docs.blender.org/api/current/bpy.types.Mask.html#bpy.types.Mask.animation_data)\n\n[Material.animation_data](https://docs.blender.org/api/current/bpy.types.Material.html#bpy.types.Material.animation_data)\n\n[Mesh.animation_data](https://docs.blender.org/api/current/bpy.types.Mesh.html#bpy.types.Mesh.animation_data)\n\n[MetaBall.animation_data](https://docs.blender.org/api/current/bpy.types.MetaBall.html#bpy.types.MetaBall.animation_data)\n\n[MovieClip.animation_data](https://docs.blender.org/api/current/bpy.types.MovieClip.html#bpy.types.MovieClip.animation_data)\n\n[NodeTree.animation_data](https://docs.blender.org/api/current/bpy.types.NodeTree.html#bpy.types.NodeTree.animation_data)\n\n[Object.animation_data](https://docs.blender.org/api/current/bpy.types.Object.html#bpy.types.Object.animation_data)\n\n[ParticleSettings.animation_data](https://docs.blender.org/api/current/bpy.types.ParticleSettings.html#bpy.types.ParticleSettings.animation_data)\n\n[PointCloud.animation_data](https://docs.blender.org/api/current/bpy.types.PointCloud.html#bpy.types.PointCloud.animation_data)\n\n[Scene.animation_data](https://docs.blender.org/api/current/bpy.types.Scene.html#bpy.types.Scene.animation_data)\n\n[Speaker.animation_data](https://docs.blender.org/api/current/bpy.types.Speaker.html#bpy.types.Speaker.animation_data)\n\n[Texture.animation_data](https://docs.blender.org/api/current/bpy.types.Texture.html#bpy.types.Texture.animation_data)\n\n[Volume.animation_data](https://docs.blender.org/api/current/bpy.types.Volume.html#bpy.types.Volume.animation_data)\n\n[World.animation_data](https://docs.blender.org/api/current/bpy.types.World.html#bpy.types.World.animation_data)"
+}
+```
+User decision: **accepted**
+
+Status: **completed**
+
+
+
+
+
+---
+
+
+
+Tool use: **web_fetch**
+
+
+Parameters:
+
+```json
+{
+ "url": "https://docs.blender.org/api/current/bpy.types.ActionSlot.html"
+}
+```
+Result:
+
+```json
+{
+ "url": "https://docs.blender.org/api/current/bpy.types.ActionSlot.html",
+ "markdown": "ActionSlot(bpy_struct) - Blender Python API\n\n# ActionSlot(bpy_struct)¶\n\nbase class — [bpy_struct](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct)\n\nclass bpy.types.ActionSlot(bpy_struct)¶\n\nIdentifier for a set of channels in this Action, that can be used by a data-block to specify what it gets animated by\n\nactive¶\n\nWhether this is the active slot, can be set by assigning to action.slots.active\n\nType:\n\nboolean, default False, (readonly)\n\nhandle¶\n\nNumber specific to this Slot, unique within the Action. This is used, for example, on a ActionKeyframeStrip to look up the ActionChannelbag for this Slot\n\nType:\n\nint in [-inf, inf], default 0, (readonly)\n\nidentifier¶\n\nUsed when connecting an Action to a data-block, to find the correct slot handle. This is the display name, prefixed by two characters determined by the slot’s ID type\n\nType:\n\nstring, default “”, (never None)\n\nname_display¶\n\nName of the slot, for display in the user interface. This name combined with the slot’s data-block type is unique within its Action\n\nType:\n\nstring, default “”, (never None)\n\nselect¶\n\nSelection state of the slot\n\nType:\n\nboolean, default False\n\nshow_expanded¶\n\nExpanded state of the slot\n\nType:\n\nboolean, default False\n\ntarget_id_type¶\n\nType of data-block that this slot is intended to animate; can be set when ‘UNSPECIFIED’ but is otherwise read-only\n\n`ACTION` Action.\n\n`ARMATURE` Armature.\n\n`BRUSH` Brush.\n\n`CACHEFILE` Cache File.\n\n`CAMERA` Camera.\n\n`COLLECTION` Collection.\n\n`CURVE` Curve.\n\n`CURVES` Curves.\n\n`FONT` Font.\n\n`GREASEPENCIL` Grease Pencil.\n\n`GREASEPENCIL_V3` Grease Pencil v3.\n\n`IMAGE` Image.\n\n`KEY` Key.\n\n`LATTICE` Lattice.\n\n`LIBRARY` Library.\n\n`LIGHT` Light.\n\n`LIGHT_PROBE` Light Probe.\n\n`LINESTYLE` Line Style.\n\n`MASK` Mask.\n\n`MATERIAL` Material.\n\n`MESH` Mesh.\n\n`META` Metaball.\n\n`MOVIECLIP` Movie Clip.\n\n`NODETREE` Node Tree.\n\n`OBJECT` Object.\n\n`PAINTCURVE` Paint Curve.\n\n`PALETTE` Palette.\n\n`PARTICLE` Particle.\n\n`POINTCLOUD` Point Cloud.\n\n`SCENE` Scene.\n\n`SCREEN` Screen.\n\n`SOUND` Sound.\n\n`SPEAKER` Speaker.\n\n`TEXT` Text.\n\n`TEXTURE` Texture.\n\n`VOLUME` Volume.\n\n`WINDOWMANAGER` Window Manager.\n\n`WORKSPACE` Workspace.\n\n`WORLD` World.\n\n`UNSPECIFIED` Unspecified – Not yet specified. When this slot is first assigned to a data-block, this will be set to the type of that data-block.\n\nType:\n\nenum in [`'ACTION'`,`'ARMATURE'`,`'BRUSH'`,`'CACHEFILE'`,`'CAMERA'`,`'COLLECTION'`,`'CURVE'`,`'CURVES'`,`'FONT'`,`'GREASEPENCIL'`,`'GREASEPENCIL_V3'`,`'IMAGE'`,`'KEY'`,`'LATTICE'`,`'LIBRARY'`,`'LIGHT'`,`'LIGHT_PROBE'`,`'LINESTYLE'`,`'MASK'`,`'MATERIAL'`,`'MESH'`,`'META'`,`'MOVIECLIP'`,`'NODETREE'`,`'OBJECT'`,`'PAINTCURVE'`,`'PALETTE'`,`'PARTICLE'`,`'POINTCLOUD'`,`'SCENE'`,`'SCREEN'`,`'SOUND'`,`'SPEAKER'`,`'TEXT'`,`'TEXTURE'`,`'VOLUME'`,`'WINDOWMANAGER'`,`'WORKSPACE'`,`'WORLD'`,`'UNSPECIFIED'`], default`'UNSPECIFIED'`\n\ntarget_id_type_icon¶ Type:\n\nint in [-inf, inf], default 0, (readonly)\n\nusers()¶\n\nReturn the data-blocks that are animated by this slot of this action\n\nReturns:\n\nusers\n\nReturn type:\n\n[bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of [ID](https://docs.blender.org/api/current/bpy.types.ID.html#bpy.types.ID)\n\nduplicate()¶\n\nDuplicate this slot, including all the animation data associated with it\n\nReturns:\n\nDuplicated Slot, The slot created by duplicating this one\n\nReturn type:\n\n`ActionSlot`\n\nclassmethod bl_rna_get_subclass(id, default=None, /)¶ Parameters:\n\nid (str) – The RNA type identifier.\n\nReturns:\n\nThe RNA type or default when not found.\n\nReturn type:\n\n[bpy.types.Struct](https://docs.blender.org/api/current/bpy.types.Struct.html#bpy.types.Struct) subclass\n\nclassmethod bl_rna_get_subclass_py(id, default=None, /)¶ Parameters:\n\nid (str) – The RNA type identifier.\n\nReturns:\n\nThe class or default when not found.\n\nReturn type:\n\ntype\n\n## Inherited Properties¶\n\n[bpy_struct.id_data](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_data)\n\n## Inherited Functions¶\n\n[bpy_struct.as_pointer](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.as_pointer)\n\n[bpy_struct.driver_add](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.driver_add)\n\n[bpy_struct.driver_remove](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.driver_remove)\n\n[bpy_struct.get](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.get)\n\n[bpy_struct.id_properties_clear](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_clear)\n\n[bpy_struct.id_properties_ensure](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_ensure)\n\n[bpy_struct.id_properties_ui](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_ui)\n\n[bpy_struct.is_property_hidden](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_hidden)\n\n[bpy_struct.is_property_overridable_library](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_overridable_library)\n\n[bpy_struct.is_property_readonly](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_readonly)\n\n[bpy_struct.is_property_set](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_set)\n\n[bpy_struct.items](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items)\n\n[bpy_struct.keyframe_delete](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keyframe_delete)\n\n[bpy_struct.keyframe_insert](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keyframe_insert)\n\n[bpy_struct.keys](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keys)\n\n[bpy_struct.path_from_id](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_from_id)\n\n[bpy_struct.path_from_module](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_from_module)\n\n[bpy_struct.path_resolve](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_resolve)\n\n[bpy_struct.pop](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.pop)\n\n[bpy_struct.property_overridable_library_set](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.property_overridable_library_set)\n\n[bpy_struct.property_unset](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.property_unset)\n\n[bpy_struct.rna_ancestors](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.rna_ancestors)\n\n[bpy_struct.type_recast](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.type_recast)\n\n[bpy_struct.values](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.values)\n\n## References¶\n\n[Action.slots](https://docs.blender.org/api/current/bpy.types.Action.html#bpy.types.Action.slots)\n\n[ActionChannelbag.slot](https://docs.blender.org/api/current/bpy.types.ActionChannelbag.html#bpy.types.ActionChannelbag.slot)\n\n[ActionChannelbags.new](https://docs.blender.org/api/current/bpy.types.ActionChannelbags.html#bpy.types.ActionChannelbags.new)\n\n[ActionConstraint.action_slot](https://docs.blender.org/api/current/bpy.types.ActionConstraint.html#bpy.types.ActionConstraint.action_slot)\n\n[ActionConstraint.action_suitable_slots](https://docs.blender.org/api/current/bpy.types.ActionConstraint.html#bpy.types.ActionConstraint.action_suitable_slots)\n\n[ActionKeyframeStrip.channelbag](https://docs.blender.org/api/current/bpy.types.ActionKeyframeStrip.html#bpy.types.ActionKeyframeStrip.channelbag)\n\n[ActionKeyframeStrip.key_insert](https://docs.blender.org/api/current/bpy.types.ActionKeyframeStrip.html#bpy.types.ActionKeyframeStrip.key_insert)\n\n`ActionSlot.duplicate`\n\n[ActionSlots.active](https://docs.blender.org/api/current/bpy.types.ActionSlots.html#bpy.types.ActionSlots.active)\n\n[ActionSlots.new](https://docs.blender.org/api/current/bpy.types.ActionSlots.html#bpy.types.ActionSlots.new)\n\n[ActionSlots.remove](https://docs.blender.org/api/current/bpy.types.ActionSlots.html#bpy.types.ActionSlots.remove)\n\n[AnimData.action_slot](https://docs.blender.org/api/current/bpy.types.AnimData.html#bpy.types.AnimData.action_slot)\n\n[AnimData.action_suitable_slots](https://docs.blender.org/api/current/bpy.types.AnimData.html#bpy.types.AnimData.action_suitable_slots)\n\n[NlaStrip.action_slot](https://docs.blender.org/api/current/bpy.types.NlaStrip.html#bpy.types.NlaStrip.action_slot)\n\n[NlaStrip.action_suitable_slots](https://docs.blender.org/api/current/bpy.types.NlaStrip.html#bpy.types.NlaStrip.action_suitable_slots)"
+}
+```
+User decision: **accepted**
+
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+So AnimData has:
+- `action` - the active action
+- `action_slot` - The slot identifies which sub-set of the Action is considered to be for this data-block
+- `action_slot_handle` - A number that identifies which sub-set
+- `action_blend_type`
+- `action_extrapolation`
+- `action_influence`
+- `last_slot_identifier` - string
+- `use_nla`
+
+When we set only `rep.animation_data.action = active_action`, Blender might not automatically set the action_slot to match the original. The action can have multiple slots (e.g. for different ID types), and the animdata's action_slot points to which slot in that action is used. So we need to copy from orig.animation_data to rep.animation_data:
+1. action (already done)
+2. action_slot - we need to set the same slot. The slot is an ActionSlot from the action - so we need to set rep's action_slot to the slot in the action that matches the original's. Since we're assigning the same action, the slots are the same action's slots. So we could set rep.animation_data.action_slot = orig.animation_data.action_slot (if they share the same action now). But wait - we're setting rep.animation_data.action = active_action (same as orig). So the action is the same. The action_slot on orig points to a slot inside that action. We need to set rep.animation_data.action_slot to the same slot. So after setting rep.animation_data.action = active_action, we should set rep.animation_data.action_slot = orig.animation_data.action_slot (the original's slot reference - which is a slot in the same action). So that should work.
+3. action_blend_type, action_extrapolation, action_influence - copy these too.
+
+Let me also check - does AnimData have action_slot in 4.4 only? The user is on Blender 5.0. I'll use getattr to be safe for older Blender.
+
+I'll update the no-NLA branch in run_mig_nla to:
+1. Set rep.animation_data.action = active_action
+2. If hasattr(orig.animation_data, 'action_slot') and orig.animation_data.action_slot and getattr(rep.animation_data, 'action_slot', None) is not None: rep.animation_data.action_slot = orig.animation_data.action_slot
+3. Copy action_blend_type, action_extrapolation, action_influence if they exist
+
+Actually re-reading the API: action_slot is "The slot identifies which sub-set of the Action is considered to be for this data-block". So when we assign the same action to rep, the action's slots are the same. So we set rep.animation_data.action_slot = orig.animation_data.action_slot (so the replacement uses the same "sub-set" of the action as the original). That should work since both are now using the same action and we're copying the slot reference.
+
+Let me implement with getattr for backward compatibility in case someone uses an older Blender.
+
+
+---
+
+
+Copying the original's action slot and related AnimData properties in the no-NLA path. AnimData has `action_slot`, `action_blend_type`, `action_extrapolation`, and `action_influence`.
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+I need to after setting rep.animation_data.action = active_action also copy:
+1. action_slot (so the same subset of the action is used)
+2. action_blend_type, action_extrapolation, action_influence
+
+Use getattr/setattr for backward compatibility with Blender versions that don't have action_slot (pre-4.4).
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+
+Done. I copied action_slot (so the same slot in the action is used on the replacement) and the blend/extrapolation/influence props. Uses getattr/hasattr so older Blender without action_slot still runs.
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **read_lints**
+
+
+Parameters:
+
+```json
+{
+ "paths": [
+ "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ ]
+}
+```
+Result:
+
+```json
+{
+ "linterErrorsByFile": [
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+Good. Summary for the user.
+
+
+---
+
+
+Update is in MigNLA’s no-NLA path.
+
+When there are no anim layers but there is an active action, we now copy the **action slot** and related AnimData from the original to the replacement:
+
+1. **`action_slot`** – So the replacement uses the same slot of the action as the original (Blender 4.4+ slotted actions).
+2. **`action_blend_type`**, **`action_extrapolation`**, **`action_influence`** – So blend mode, extrapolation, and influence match.
+
+All of this is guarded with `getattr`/`hasattr` so it only runs when those properties exist (e.g. Blender 4.4+). Reload the add-on and run MigNLA again; the replacement armature’s action slot should match the original.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt add debug, it's still not migrating the slot. https://docs.blender.org/api/current/bpy.ops.action.html
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+
+
+---
+
+
+
+---
+
+
+
+Tool use: **read_file_v2**
+
+Status: **loading**
+
+
+
+
+
+---
+
+
+
+---
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+Status: **loading**
+
+
+
+
+
+---
+
diff --git a/ops/migrator.py b/ops/migrator.py
index 7551baa..0eeb837 100644
--- a/ops/migrator.py
+++ b/ops/migrator.py
@@ -64,20 +64,53 @@ def _mirror_als_turn_on(orig, rep):
pass
-def run_mig_nla(orig, rep):
- """Migrate NLA: copy tracks and strips to replacement; or apply active action only (MigNLA)."""
+def run_mig_nla(orig, rep, report=None):
+ """Migrate NLA: copy tracks and strips to replacement; or mirror action slot when no NLA (MigNLA)."""
if not orig.animation_data:
return
ad = orig.animation_data
has_nla = ad.nla_tracks and len(ad.nla_tracks) > 0
active_action = getattr(ad, "action", None)
- if not has_nla and active_action:
+ if not has_nla:
if rep.animation_data is None:
rep.animation_data_create()
- rep.animation_data.action = active_action
+ rad = rep.animation_data
+ # Debug: Orig action slot state (Blender 4.4+ slotted actions).
+ def _slot_debug(label, animdata):
+ if animdata is None:
+ print(f"[DLM MigNLA] {label}: no animation_data")
+ return
+ a = getattr(animdata, "action", None)
+ print(f"[DLM MigNLA] {label} action={a.name if a else None}")
+ for p in ("action_slot", "action_slot_handle", "last_slot_identifier",
+ "action_blend_type", "action_extrapolation", "action_influence"):
+ if hasattr(animdata, p):
+ v = getattr(animdata, p, None)
+ if hasattr(v, "identifier"):
+ v = getattr(v, "identifier", v)
+ print(f"[DLM MigNLA] {p}={v!r}")
+ _slot_debug("Orig (before)", ad)
+ _slot_debug("Rep (before)", rad)
+ # Copy last_slot_identifier before action so slot is resolved when assigning (4.4+).
+ if hasattr(ad, "last_slot_identifier") and hasattr(rad, "last_slot_identifier") and ad.last_slot_identifier:
+ rad.last_slot_identifier = ad.last_slot_identifier
+ print(f"[DLM MigNLA] set rep last_slot_identifier={ad.last_slot_identifier!r}")
+ rad.action = active_action
+ # Copy Action Slot and related props (Blender 4.4+ slotted actions).
+ if getattr(ad, "action_slot", None) and getattr(rad, "action_slot", None):
+ try:
+ rad.action_slot = ad.action_slot
+ print(f"[DLM MigNLA] set rep action_slot={getattr(ad.action_slot, 'identifier', ad.action_slot)!r}")
+ except Exception as e:
+ print(f"[DLM MigNLA] rad.action_slot assign failed: {e}")
+ for prop in ("action_blend_type", "action_extrapolation", "action_influence"):
+ if hasattr(ad, prop) and hasattr(rad, prop):
+ setattr(rad, prop, getattr(ad, prop))
+ print(f"[DLM MigNLA] set rep {prop}={getattr(ad, prop)!r}")
+ _slot_debug("Rep (after)", rad)
_mirror_als_turn_on(orig, rep)
- return
- if not has_nla:
+ if report:
+ report({"INFO"}, "No NLA detected, active action and slot copied to Replacement Armature.")
return
if rep.animation_data is None:
rep.animation_data_create()
@@ -122,6 +155,13 @@ def run_mig_nla(orig, rep):
new_strip.use_sync_length = strip.use_sync_length
prev_track = new_track
_mirror_als_turn_on(orig, rep)
+ if report:
+ key = "als.turn_on"
+ has_als = key in orig or any(key in pb for pb in orig.pose.bones)
+ if has_als:
+ report({"INFO"}, "NLA layers detected, Animation Layer attributes migrated to Replacement Armature.")
+ else:
+ report({"INFO"}, "NLA layers detected and migrated. No Animation Layers found.")
EXCLUDE_PROPS = {"_RNA_UI", "rigify_type", "rigify_parameters"}
diff --git a/ui/operators.py b/ui/operators.py
index 720cb36..87949c0 100644
--- a/ui/operators.py
+++ b/ui/operators.py
@@ -302,8 +302,7 @@ class DLM_OT_migrator_migrate_nla(Operator):
return {"CANCELLED"}
try:
from ..ops.migrator import run_mig_nla
- run_mig_nla(orig, rep)
- self.report({"INFO"}, "Migrate NLA done.")
+ run_mig_nla(orig, rep, report=self.report)
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, str(e))