Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cc04846
ci: add team-gated Cursor review (thin caller for github-workflows)
mattmillerai Jun 15, 2026
4cadbb8
fix: resolve review feedback
mattmillerai Jun 15, 2026
e498c4a
ci: bump cursor-review SHA to github-workflows#9, drop judge_model ov…
mattmillerai Jun 17, 2026
880582a
[automated] Apply ESLint and Oxfmt fixes
actions-user Jun 17, 2026
8d23daa
fix: resolve review feedback
mattmillerai Jun 17, 2026
1fee449
Merge branch 'main' into ci/cursor-review-workflow
mattmillerai Jun 17, 2026
5ef89c7
feat(assets): adopt cursor pagination in the Generated tab jobs walk
mattmillerai Jun 10, 2026
1f810a1
fix(assets): harden cursor pagination against review findings
mattmillerai Jun 10, 2026
69a4d78
fix(assets): guard the history jobs walk against a non-advancing cursor
mattmillerai Jun 16, 2026
f318718
docs(assets): add JSDoc to assetsStore per review
mattmillerai Jun 17, 2026
753b0b4
fix(assets): harden epoch guards and truncate JobsApiError body
mattmillerai Jun 19, 2026
4290619
[automated] Apply ESLint and Oxfmt fixes
actions-user Jun 19, 2026
c207f56
Merge branch 'main' into matt/fe-962-fe-adopt-cursor-pagination-in-th…
mattmillerai Jun 19, 2026
89fdbcd
Update src/stores/assetsStore.ts
mattmillerai Jun 22, 2026
c238145
Update src/platform/remote/comfyui/jobs/jobTypes.ts
mattmillerai Jun 22, 2026
6f7686b
Update src/platform/remote/comfyui/jobs/fetchJobs.ts
mattmillerai Jun 22, 2026
6c3ead5
Update src/stores/assetsStore.ts
mattmillerai Jun 22, 2026
ea0f8a9
test: add body-truncation coverage and historyError-null assertion fo…
mattmillerai Jun 22, 2026
8b40f6a
fix: reset offset to 0 on cursor-recovery fallback to prevent page drift
mattmillerai Jun 22, 2026
e471b64
Merge remote-tracking branch 'origin/main' into HEAD
mattmillerai Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/pr-cursor-review.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Description: Team-gated multi-model Cursor review — a thin caller for the
# reusable workflow in Comfy-Org/github-workflows, which is the single source of
# truth for the panel, judge, prompts, and scripts. Triggered by the
# 'cursor-review' label.
#
# Access control (team-only, two layers):
# 1. Only users with triage permission or higher can apply a label in a public
# repo, so the public cannot trigger this.
# 2. The reusable workflow's secret-bearing jobs do not run on fork PRs (forks
# get no secrets), so CURSOR_API_KEY is reachable only on internal branches.
name: 'PR: Cursor Review'

on:
pull_request:
types: [labeled, unlabeled]

permissions:
contents: read
pull-requests: write

concurrency:
# Re-labeling cancels an in-flight run for the same PR + label.
group: cursor-review-pr-${{ github.event.pull_request.number }}-${{ github.event.label.name }}
cancel-in-progress: true

jobs:
cursor-review:
if: github.event.action == 'labeled' && github.event.label.name == 'cursor-review'
# SHA-pinned per zizmor `unpinned-uses: hash-pin`. Bump this SHA to pick up
# upstream changes; keep `workflows_ref` matching so prompts/scripts load
# from the same commit as the workflow definition.
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@047ca48febe3a6647608ed2e0c4331b491cb9d6a # github-workflows#9
with:
# Overriding diff_excludes replaces the reusable default wholesale, so
# this restates the generated/vendored defaults and adds this repo's heavy
# paths (Playwright snapshots, generated manager types).
diff_excludes: >-
:!**/package-lock.json
:!**/yarn.lock
:!**/pnpm-lock.yaml
:!**/node_modules/**
:!**/.claude/**
:!**/dist/**
:!**/vendor/**
:!**/*.generated.*
:!**/*.min.js
:!**/*.min.css
:!**/*-snapshots/**
:!src/workbench/extensions/manager/types/generatedManagerTypes.ts
# Load the prompts/scripts from the same ref as `uses:`.
workflows_ref: 047ca48febe3a6647608ed2e0c4331b491cb9d6a
secrets:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
# Optional — enables start/complete Slack DMs to the triggerer.
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
2 changes: 1 addition & 1 deletion src/components/queue/QueueProgressOverlay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ const focusAssetInSidebar = async (item: JobListItem) => {
const assetId = String(jobId)
openAssetsSidebar()
await nextTick()
await assetsStore.updateHistory()
await assetsStore.refreshHistoryHead()
const asset = assetsStore.historyAssets.find(
(existingAsset) => existingAsset.id === assetId
)
Expand Down
4 changes: 2 additions & 2 deletions src/platform/missingMedia/missingMediaAssetResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,13 @@ describe('resolveMissingMediaAssetSources', () => {
1,
expect.any(Function),
200,
0
{ offset: 0 }
)
expect(mockFetchHistoryPage).toHaveBeenNthCalledWith(
2,
expect.any(Function),
200,
200
{ offset: 200 }
)
})

Expand Down
2 changes: 1 addition & 1 deletion src/platform/missingMedia/missingMediaAssetResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ async function fetchGeneratedHistoryAssets(
const historyPage = await fetchHistoryPage(
api.fetchApi.bind(api),
HISTORY_MEDIA_ASSETS_PAGE_SIZE,
requestedOffset
{ offset: requestedOffset }
Comment thread
mattmillerai marked this conversation as resolved.
)

signal?.throwIfAborted()
Expand Down
6 changes: 3 additions & 3 deletions src/platform/missingMedia/missingMediaScan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ describe('verifyMediaCandidates', () => {
expect(mockFetchHistoryPage).toHaveBeenCalledWith(
expect.any(Function),
200,
0
{ offset: 0 }
)
expect(candidates[0]).toMatchObject({
name: 'subfolder/photo.png [output]',
Expand Down Expand Up @@ -843,13 +843,13 @@ describe('verifyMediaCandidates', () => {
1,
expect.any(Function),
200,
0
{ offset: 0 }
)
expect(mockFetchHistoryPage).toHaveBeenNthCalledWith(
2,
expect.any(Function),
200,
200
{ offset: 200 }
)
expect(candidates[0].isMissing).toBe(false)
})
Expand Down
116 changes: 102 additions & 14 deletions src/platform/remote/comfyui/jobs/fetchJobs.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'

import {
JobsApiError,
extractWorkflow,
fetchHistory,
fetchHistoryPage,
Expand Down Expand Up @@ -39,7 +40,8 @@ function createMockResponse(
offset: pagination.offset ?? 0,
limit: pagination.limit ?? 200,
total,
has_more: pagination.has_more ?? false
has_more: pagination.has_more ?? false,
next_cursor: pagination.next_cursor
}
}
}
Expand Down Expand Up @@ -135,23 +137,41 @@ describe('fetchJobs', () => {
expect(result[0].priority).toBe(999)
})

it('returns empty array on error', async () => {
it('propagates fetch errors', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))

const result = await fetchHistory(mockFetch)

expect(result).toEqual([])
await expect(fetchHistory(mockFetch)).rejects.toThrow('Network error')
})

it('returns empty array on non-ok response', async () => {
it('throws a JobsApiError carrying status and body on non-ok response', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 500
status: 400,
text: () =>
Promise.resolve('{"error":"Invalid cursor","code":"INVALID_CURSOR"}')
})

const result = await fetchHistory(mockFetch)
await expect(fetchHistory(mockFetch)).rejects.toBeInstanceOf(JobsApiError)
await expect(fetchHistory(mockFetch)).rejects.toMatchObject({
status: 400,
message: expect.stringContaining('INVALID_CURSOR')
})
})

it('parses a null next_cursor as absent', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(
createMockResponse([createMockJob('job1', 'completed')], 1, {
next_cursor: null
})
)
})

expect(result).toEqual([])
const result = await fetchHistoryPage(mockFetch, 200, { offset: 0 })

expect(result.nextCursor).toBeUndefined()
})

it('parses batch containing text-only preview outputs', async () => {
Expand Down Expand Up @@ -205,7 +225,7 @@ describe('fetchJobs', () => {
)
})

const result = await fetchHistoryPage(mockFetch, 2, 5)
const result = await fetchHistoryPage(mockFetch, 2, { offset: 5 })

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=2&offset=5'
Expand All @@ -218,6 +238,76 @@ describe('fetchJobs', () => {
expect(result.jobs[0].priority).toBe(5)
expect(result.jobs[1].priority).toBe(4)
})

it('sends the cursor instead of offset and returns next_cursor', async () => {
const mockFetch = vi.fn().mockResolvedValue({
Comment thread
mattmillerai marked this conversation as resolved.
Outdated
ok: true,
json: () =>
Promise.resolve(
createMockResponse([createMockJob('job1', 'completed')], 10, {
has_more: true,
next_cursor: 'cursor-page-2'
})
)
})

const result = await fetchHistoryPage(mockFetch, 200, {
after: 'cursor-page-1'
})

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=200&after=cursor-page-1'
)
expect(result.nextCursor).toBe('cursor-page-2')
expect(result.hasMore).toBe(true)
})

it('uri-encodes the cursor', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockResponse([]))
})

await fetchHistoryPage(mockFetch, 200, { after: 'a+b/c=' })

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=200&after=a%2Bb%2Fc%3D'
)
})

it('returns next_cursor from offset-mode responses for cursor bootstrap', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(
createMockResponse([createMockJob('job1', 'completed')], 10, {
has_more: true,
next_cursor: 'minted-in-offset-mode'
})
)
})

const result = await fetchHistoryPage(mockFetch, 200, { offset: 0 })

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=200&offset=0'
)
expect(result.nextCursor).toBe('minted-in-offset-mode')
})

it('omits nextCursor when the server does not mint one', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(
createMockResponse([createMockJob('job1', 'completed')])
)
})

const result = await fetchHistoryPage(mockFetch, 200, { offset: 0 })

expect(result.nextCursor).toBeUndefined()
})
})

describe('fetchQueue', () => {
Expand Down Expand Up @@ -268,12 +358,10 @@ describe('fetchJobs', () => {
)
})

it('returns empty arrays on error', async () => {
it('propagates fetch errors', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))

const result = await fetchQueue(mockFetch)

expect(result).toEqual({ Running: [], Pending: [] })
await expect(fetchQueue(mockFetch)).rejects.toThrow('Network error')
})
})

Expand Down
Loading
Loading