From 7b701ad07b1c34d121448e21d6f8b5c13ef07d73 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 21 Jan 2026 09:16:13 -0800 Subject: [PATCH 01/17] fix: Consistent keydown handling for EditableText and TagsInput escape key (#8204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR improves keyboard event handling consistency and fixes an issue where pressing Escape in nested input components would unintentionally close parent modals/dialogs. ## Changes ### Keyboard Event Fixes **TagsInput Escape Key Handling** - Added `@keydown.escape.stop` handler to `TagsInputInput.vue` to prevent Escape from bubbling up and closing parent modals - The handler blurs the input and exits editing mode without propagating the event **EditableText keyup → keydown Migration** - Changed `@keyup.enter` to `@keydown.enter` and `@keyup.escape` to `@keydown.escape` - Using `keydown` is more consistent with how other UI frameworks handle these events and provides more responsive feedback - Updated corresponding unit tests to use `keydown` triggers ### Why keydown over keyup? - `keydown` fires immediately when the key is pressed, providing faster perceived response - Better consistency with browser/OS conventions for action triggers - Prevents default behaviors (like form submission) more reliably when needed - Aligns with other keyboard handlers in the codebase ## Testing - Updated `EditableText.test.ts` to use `keydown` events - Updated `NodeHeader.test.ts` to use `keydown.enter` - Manual testing: Escape in TagsInput no longer closes parent modal ## Checklist - [x] Unit tests updated - [x] Keyboard event handlers consistent - [x] No breaking changes to component API ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8204-fix-Consistent-keydown-handling-for-EditableText-and-TagsInput-escape-key-2ef6d73d365081f0ac6bed8bcae57657) by [Unito](https://www.unito.io) --- src/components/common/EditableText.test.ts | 10 +++++----- src/components/common/EditableText.vue | 6 +++--- src/components/ui/tags-input/TagsInputInput.vue | 6 ++++++ .../extensions/vueNodes/components/NodeHeader.test.ts | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/common/EditableText.test.ts b/src/components/common/EditableText.test.ts index 2d31123b9..df1ea5152 100644 --- a/src/components/common/EditableText.test.ts +++ b/src/components/common/EditableText.test.ts @@ -51,7 +51,7 @@ describe('EditableText', () => { isEditing: true }) await wrapper.findComponent(InputText).setValue('New Text') - await wrapper.findComponent(InputText).trigger('keyup.enter') + await wrapper.findComponent(InputText).trigger('keydown.enter') // Blur event should have been triggered expect(wrapper.findComponent(InputText).element).not.toBe( document.activeElement @@ -79,7 +79,7 @@ describe('EditableText', () => { await wrapper.findComponent(InputText).setValue('Modified Text') // Press escape - await wrapper.findComponent(InputText).trigger('keyup.escape') + await wrapper.findComponent(InputText).trigger('keydown.escape') // Should emit cancel event expect(wrapper.emitted('cancel')).toBeTruthy() @@ -103,7 +103,7 @@ describe('EditableText', () => { await wrapper.findComponent(InputText).setValue('Modified Text') // Press escape (which triggers blur internally) - await wrapper.findComponent(InputText).trigger('keyup.escape') + await wrapper.findComponent(InputText).trigger('keydown.escape') // Manually trigger blur to simulate the blur that happens after escape await wrapper.findComponent(InputText).trigger('blur') @@ -120,7 +120,7 @@ describe('EditableText', () => { isEditing: true }) await enterWrapper.findComponent(InputText).setValue('Saved Text') - await enterWrapper.findComponent(InputText).trigger('keyup.enter') + await enterWrapper.findComponent(InputText).trigger('keydown.enter') // Trigger blur that happens after enter await enterWrapper.findComponent(InputText).trigger('blur') expect(enterWrapper.emitted('edit')).toBeTruthy() @@ -133,7 +133,7 @@ describe('EditableText', () => { isEditing: true }) await escapeWrapper.findComponent(InputText).setValue('Cancelled Text') - await escapeWrapper.findComponent(InputText).trigger('keyup.escape') + await escapeWrapper.findComponent(InputText).trigger('keydown.escape') expect(escapeWrapper.emitted('cancel')).toBeTruthy() expect(escapeWrapper.emitted('edit')).toBeFalsy() }) diff --git a/src/components/common/EditableText.vue b/src/components/common/EditableText.vue index 322d332a4..440935fb9 100644 --- a/src/components/common/EditableText.vue +++ b/src/components/common/EditableText.vue @@ -3,7 +3,7 @@ {{ modelValue }} - + isEditing.value || isEmpty) const { forwardRef, currentElement } = useForwardExpose() const registerFocus = inject(tagsInputFocusKey, undefined) +function handleEscape() { + currentElement.value?.blur() + isEditing.value = false +} + onMounted(() => { registerFocus?.(() => currentElement.value?.focus()) }) @@ -44,5 +49,6 @@ onUnmounted(() => { className ) " + @keydown.escape.stop="handleEscape" /> diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts index 0c755116b..a813e88af 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts @@ -154,7 +154,7 @@ describe('NodeHeader.vue', () => { // Edit and confirm (EditableText uses blur or enter to emit) const input = wrapper.get('[data-testid="node-title-input"]') await input.setValue('My Custom Sampler') - await input.trigger('keyup.enter') + await input.trigger('keydown.enter') await input.trigger('blur') // NodeHeader should emit update:title with trimmed value @@ -169,7 +169,7 @@ describe('NodeHeader.vue', () => { await wrapper.get('[data-testid="node-header-1"]').trigger('dblclick') const input = wrapper.get('[data-testid="node-title-input"]') await input.setValue('Should Not Save') - await input.trigger('keyup.escape') + await input.trigger('keydown.escape') // Should not emit update:title expect(wrapper.emitted('update:title')).toBeFalsy() From fcf2e0e639850b7e1a41b93d8eafd3c08b1e2d69 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 21 Jan 2026 11:48:22 -0800 Subject: [PATCH 02/17] feat: add badge support to NavItem component (#8207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds optional badge support to the `NavItem` component and `NavItemData` interface, enabling navigation items in the left sidebar of the Asset Browser Modal to display counts or status indicators. ## Changes - **`src/types/navTypes.ts`**: Added optional `badge?: string | number` property to `NavItemData` interface - **`src/components/widget/nav/NavItem.vue`**: Added `StatusBadge` rendering when `badge` prop is provided - **`src/components/widget/panel/LeftSidePanel.vue`**: Wired `badge` prop from `NavItemData` to `NavItem` for both grouped and ungrouped items ## Usage ```ts const navItem: NavItemData = { id: 'assets', label: 'Assets', icon: 'pi pi-folder', badge: 5 // Optional - displays count badge } ``` ## Related - Builds on #8170 which added queue badge functionality ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8207-feat-add-badge-support-to-NavItem-component-2ef6d73d365081669f86fe2fc618e87f) by [Unito](https://www.unito.io) --- src/components/widget/nav/NavItem.vue | 19 +++++++++++++------ src/components/widget/panel/LeftSidePanel.vue | 2 ++ src/types/navTypes.ts | 1 + 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/widget/nav/NavItem.vue b/src/components/widget/nav/NavItem.vue index 8c20ad929..eb20b9852 100644 --- a/src/components/widget/nav/NavItem.vue +++ b/src/components/widget/nav/NavItem.vue @@ -5,7 +5,7 @@ disabled: !isOverflowing, pt: { text: { class: 'whitespace-nowrap' } } }" - class="flex cursor-pointer items-start gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground" + class="flex cursor-pointer items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground" :class=" active ? 'bg-interface-menu-component-surface-selected' @@ -15,25 +15,32 @@ @mouseenter="checkOverflow" @click="onClick" > -
- -
+ - + + diff --git a/src/locales/en/main.json b/src/locales/en/main.json index ef7b858b4..749273d20 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -100,6 +100,11 @@ "no": "No", "cancel": "Cancel", "close": "Close", + "closeDialog": "Close dialog", + "showLeftPanel": "Show left panel", + "hideLeftPanel": "Hide left panel", + "showRightPanel": "Show right panel", + "hideRightPanel": "Hide right panel", "or": "or", "pressKeysForNewBinding": "Press keys for new binding", "defaultBanner": "default banner", From d12c6d7814f6aea2bb5ccfc4777ca87d076d4d37 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Wed, 21 Jan 2026 15:13:27 -0800 Subject: [PATCH 05/17] fix: sort queue jobs by create time (#8225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sort queue overlay ordering by create_time instead of priority so queued jobs keep their order when completions arrive. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8225-fix-sort-queue-jobs-by-create-time-2ef6d73d3650815a8a81ec9d0b23a4e6) by [Unito](https://www.unito.io) --- src/composables/queue/useJobList.test.ts | 28 +++++++++++++++++++----- src/composables/queue/useJobList.ts | 7 +++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/composables/queue/useJobList.test.ts b/src/composables/queue/useJobList.test.ts index a2a061f78..0aaaa892c 100644 --- a/src/composables/queue/useJobList.test.ts +++ b/src/composables/queue/useJobList.test.ts @@ -305,24 +305,40 @@ describe('useJobList', () => { expect(vi.getTimerCount()).toBe(0) }) - it('sorts all tasks by priority descending', async () => { + it('sorts all tasks by create time', async () => { queueStoreMock.pendingTasks = [ - createTask({ promptId: 'p', queueIndex: 1, mockState: 'pending' }) + createTask({ + promptId: 'p', + queueIndex: 1, + mockState: 'pending', + createTime: 3000 + }) ] queueStoreMock.runningTasks = [ - createTask({ promptId: 'r', queueIndex: 5, mockState: 'running' }) + createTask({ + promptId: 'r', + queueIndex: 5, + mockState: 'running', + createTime: 2000 + }) ] queueStoreMock.historyTasks = [ - createTask({ promptId: 'h', queueIndex: 3, mockState: 'completed' }) + createTask({ + promptId: 'h', + queueIndex: 3, + mockState: 'completed', + createTime: 1000, + executionEndTimestamp: 5000 + }) ] const { allTasksSorted } = initComposable() await flush() expect(allTasksSorted.value.map((task) => task.promptId)).toEqual([ + 'p', 'r', - 'h', - 'p' + 'h' ]) }) diff --git a/src/composables/queue/useJobList.ts b/src/composables/queue/useJobList.ts index b745a7103..aa37d99e0 100644 --- a/src/composables/queue/useJobList.ts +++ b/src/composables/queue/useJobList.ts @@ -197,13 +197,18 @@ export function useJobList() { const selectedWorkflowFilter = ref<'all' | 'current'>('all') const selectedSortMode = ref('mostRecent') + const mostRecentTimestamp = (task: TaskItemImpl) => task.createTime ?? 0 + const allTasksSorted = computed(() => { const all = [ ...queueStore.pendingTasks, ...queueStore.runningTasks, ...queueStore.historyTasks ] - return all.sort((a, b) => b.queueIndex - a.queueIndex) + return all.sort((a, b) => { + const delta = mostRecentTimestamp(b) - mostRecentTimestamp(a) + return delta === 0 ? 0 : delta + }) }) const tasksWithJobState = computed(() => From f1d17475822559642f952b2336ee958898fedee8 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 21 Jan 2026 16:32:30 -0800 Subject: [PATCH 06/17] feat: add session download tracking to assetDownloadStore (#8213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add session download tracking to track which assets were downloaded during the current session. This enables UI features like: - Badge count on "Imported" nav showing newly downloaded assets - Visual indicator on asset cards for recently downloaded items ## Changes - Add `acknowledged` flag to `AssetDownload` interface - Add `unacknowledgedDownloads` computed for filtering - Add `sessionDownloadCount` computed for badge display - Add `isDownloadedThisSession(identifier)` to check individual assets - Add `acknowledgeDownload(identifier)` to mark assets as seen ## Testing - 6 new unit tests covering all session tracking functionality - Run: `pnpm test:unit -- src/stores/assetDownloadStore.test.ts` ## Related - Part of Asset Browser improvements (#8090) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8213-feat-add-session-download-tracking-to-assetDownloadStore-2ef6d73d365081538045e8544d26bafa) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp --- src/locales/en/main.json | 6 + .../assets/components/AssetBrowserModal.vue | 9 +- .../composables/useAssetBrowser.test.ts | 158 ++++++++++-------- .../assets/composables/useAssetBrowser.ts | 83 +++++++-- src/stores/assetDownloadStore.test.ts | 76 +++++++++ src/stores/assetDownloadStore.ts | 26 ++- 6 files changed, 272 insertions(+), 86 deletions(-) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 749273d20..75afcfd07 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -2315,6 +2315,12 @@ "assetBrowser": { "allCategory": "All {category}", "allModels": "All Models", + "byType": "By type", + "emptyImported": { + "canImport": "No imported models yet. Click \"Import Model\" to add your own.", + "restricted": "Personal models are only available at Creator tier and above." + }, + "imported": "Imported", "assetCollection": "Asset collection", "assets": "Assets", "baseModels": "Base models", diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue index b7db7494c..8344a807f 100644 --- a/src/platform/assets/components/AssetBrowserModal.vue +++ b/src/platform/assets/components/AssetBrowserModal.vue @@ -7,12 +7,12 @@ >