App.command.* restarts the timeline and canvas in v1.3.15.4 (breaking script loops)

I recently updated to Aseprite v1.3.15.4 and noticed that calling any app.command.CommandName{} inside a script now restarts the entire timeline and redraws the whole canvas area, something that did not happen in the previous version (v1.3.14-beta1).

I’m not really sure if this is considered a bug or just a new implementation going forward, but it changes how automation scripts behave.


I created a simple test script that applies a command to modify the active cel image, for example using app.command.HueSaturation{} to darken it.

local sprite = app.activeSprite
if not sprite then return app.alert("No active sprite") end

local dialog = Dialog { title = "app.command.Test{}    ", onclose = function() end }

dialog:button { text = "&Test", onclick = function()
    local cel_selection = app.range
    if #cel_selection.cels == 0 then return app.alert("No cels selected") end

    -- Validate that all selected cels belong to the same layer
    local selected_layer = cel_selection.cels[1].layer
    for _, cel in ipairs(cel_selection.cels) do
        if cel.layer ~= selected_layer then return app.alert("All selected cels must be from the same layer!") end
    end

    local selected_cels = app.range.cels
    app.transaction("test appCommand", function()
        local target_layer = sprite:newLayer()
        for _, cel in ipairs(selected_cels) do
            local original_image   = cel.image:clone()
            local cropped_image
            local cropped_position = cel.position

            cropped_image          = original_image
            local new_cel          = sprite:newCel(target_layer, cel.frameNumber)
            new_cel.image          = cropped_image
            new_cel.position       = cropped_position
            app.activeCel          = new_cel

            app.command.HueSaturation {
                ui = false,
                mode = 'hsl',
                hue = 0,
                saturation = 0,
                lightness = -100
            }
        end
    end)

    app.refresh()
    app.layer = selected_layer
    app.range.layers = { selected_layer }
end
}


dialog:show { wait = false }

I designed this loop intentionally to apply the command to each cel individually instead of applying it to the entire layer, which worked perfectly before the update.


In v1.3.15.4:

  • Every time the command is executed, the timeline and canvas briefly disappear and reappear.
  • This causes noticeable flickering or reset of the drawing area.
  • When used inside a loop, the process is interrupted, the script stops midway and the console prints a “2” message (not sure what it means).

Notepad_VeAV4ZN6Vs

In v1.3.14-beta1:

  • The same script runs smoothly.
  • The timeline and canvas remain stable.
  • Commands like app.command.HueSaturation{} or app.command.NewLayer{} execute without reloading the interface, even in a loop.

Notepad_Ohr3Lndovk

Additional Notes:

  • This issue affects multiple commands (not just HueSaturation).
  • It breaks automation scripts that rely on app.command inside loops.
  • The Console window also doesn’t retain its resized position or dimensions.
    aseprite_Afq9UxBay6

I just want to confirm is this a new behavior by design, or possibly a bug/regression?
If it’s intentional, I’ll try to find a different way to run app.command inside loops.

I’ll look more into this later, but can you try it out in v1.3.15.5?

Thank you for the quick response! I’ve tested it on both v1.3.15.5 and v1.3.16-beta2, and the issue still behaves the same as in v1.3.15.4. Also, the console window still doesn’t retain its resized position or dimensions

aseprite_DkdK9JYhq2

Hi there! I’ve been busy for the past few days so I wasn’t able to test this until now. Here’s what I found in my testing:

  1. I am experiencing the same issue with the Hue/Saturation command (and the other “effects” commands) when using it in scripts and from the Edit menu. The canvas and timeline seem to disappear/refresh for a split second before appearing again. I skimmed through the changelogs and could not find a specific cause; more looking into is needed.
  2. In my testing, I found that the number printed to the console is the frame number of the second frame selected in a selection. Strangely, in another test, I found that this wasn’t the case. Weird. I could not find a cause as to why this number was printed out.
  3. I am experiencing the console issue as well.

I have made issues for these on the Github, you can watch them there for updates. Thanks for reporting them!

Hi! I did another round of testing. This time, I removed the loop from inside the transaction block. Without the transaction, the loop runs completely as expected and the commands execute in sequence.

However, removing the transaction isn’t really a solution, since the script needs it for proper undo/redo grouping. So the issue seems to occur specifically when app.command.* is executed repeatedly inside a transaction.

local sprite = app.activeSprite
if not sprite then return app.alert("No active sprite") end

local dialog = Dialog { title = "app.command.Test{}    ", onclose = function() end }

dialog:button { text = "&Test", onclick = function()
    local cel_selection = app.range
    if #cel_selection.cels == 0 then return app.alert("No cels selected") end

    -- Validate that all selected cels belong to the same layer
    local selected_layer = cel_selection.cels[1].layer
    for _, cel in ipairs(cel_selection.cels) do
        if cel.layer ~= selected_layer then return app.alert("All selected cels must be from the same layer!") end
    end

    local selected_cels = app.range.cels
    -- app.transaction("test appCommand", function()
    local target_layer = sprite:newLayer()
    for _, cel in ipairs(selected_cels) do
        local original_image   = cel.image:clone()
        local cropped_image
        local cropped_position = cel.position

        cropped_image          = original_image
        local new_cel          = sprite:newCel(target_layer, cel.frameNumber)
        new_cel.image          = cropped_image
        new_cel.position       = cropped_position
        app.activeCel          = new_cel

        app.command.HueSaturation {
            ui = false,
            mode = 'hsl',
            hue = 0,
            saturation = 0,
            lightness = -100
        }
    end
    -- end)

    app.refresh()
    app.layer = selected_layer
    app.range.layers = { selected_layer }
end
}


dialog:show { wait = false }

Notepad_2Jliya86Uz

1 Like

Hey devkidd!

Thanks for noticing this, I failed to check it in my testing.

I have updated the Github issue!

Thanks again!