Skip to content

feat(assets): adopt cursor pagination in the Generated tab jobs walk#12769

Open
mattmillerai wants to merge 20 commits into
mainfrom
matt/fe-962-fe-adopt-cursor-pagination-in-the-generated-tab-jobs-sourced
Open

feat(assets): adopt cursor pagination in the Generated tab jobs walk#12769
mattmillerai wants to merge 20 commits into
mainfrom
matt/fe-962-fe-adopt-cursor-pagination-in-the-generated-tab-jobs-sourced

Conversation

@mattmillerai

@mattmillerai mattmillerai commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

ELI-5

The Generated tab shows finished jobs newest-first and loads more as you scroll. Today it asks the server for "items 200–400" (offset paging). If a job finishes while you're scrolling, every item shifts down one slot, so the next page repeats or skips things. Worse, every time a job finished the app threw the whole list away and re-downloaded page one — goodbye scroll position.

Now the server hands back a bookmark (next_cursor) with each page and we ask for "the page after this bookmark." Bookmarks don't slip when new items arrive, so the scroll walk is drift-free. When a job finishes, we fetch just the newest page and slot the new items in at the top instead of rebuilding everything. If a bookmark goes stale (e.g. the server restarted), we fall back to the old counting method for one page and pick up a fresh bookmark from the response. Old backends that don't hand out bookmarks keep working exactly as before.

Summary

Migrates the Generated tab's jobs walk from offset to keyset cursor pagination (after / next_cursor), now that core (Comfy-Org/ComfyUI#14363) and cloud (BE-885) share one cursor contract on GET /api/jobs.

Changes

  • What:
    • fetchHistoryPage takes { offset?, after? }, sends after instead of offset when present, and surfaces next_cursor from the pagination response
    • The assets store walks history by cursor and bootstraps into cursor mode from the first offset page (the server mints a cursor either way); offset remains the fallback for backends that don't mint cursors
    • A rejected cursor (e.g. 400 INVALID_CURSOR after a server restart) is dropped and the page retried once via the offset fallback
    • fetchJobsRaw now throws on failure instead of fabricating an empty hasMore: false page, so failures are distinguishable from the end of the timeline; api.getHistory/api.getQueue already catch, the store's error paths now actually run, and the missing-media resolver propagates like its cloud branch already did
    • Job-completion events call a new refreshHistoryHead() that merge-prepends the head page instead of resetting scroll state; it replaces the list when the page spans the whole timeline (pruning server-side deletions, e.g. cleared history) and restarts the walk when >1 page of new jobs would leave an unfillable gap
    • An epoch counter invalidates in-flight page fetches when the list resets, so stale continuations can't merge into a fresh walk or move its cursor
    • hasMoreHistory is server-authoritative (pagination.has_more) instead of inferred from batch length
  • Breaking: fetchHistoryPage/fetchHistory/fetchQueue now propagate fetch failures instead of returning empty results (all callers audited and handle it)

Review Focus

  • Cursors are order-bound with no before param, so "fetch newer than X" is not expressible — head-page merge is the prepend mechanism per FE-962
  • refreshHistoryHead gap/whole-timeline decisions in assetsStore.ts
  • Deletions deeper than the head page still resync only on remount/manual refresh (unchanged eventual consistency; full reconciliation is impossible under the contract)
  • The Playwright jobs fixture never mints cursors, so e2e exercises the offset fallback path unchanged
  • Distinct from feat(assets): walk getAllAssetsByTag via keyset cursor #12720, which converts the tag-filtered getAllAssetsByTag walk in assetService — no overlapping files

Linear: FE-962

@mattmillerai mattmillerai requested a review from a team June 10, 2026 19:04
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds cursor-aware Jobs pagination (after cursor with offset fallback), throws JobsApiError on non-OK responses, rewrites assetsStore history paging with cursor state and head-refresh semantics (refreshHistoryHead), and updates callers/tests to use fetchHistoryPage and refreshHistoryHead call shapes.

Changes

Cursor Pagination & History Refresh

Layer / File(s) Summary
API pagination types and contracts
src/platform/remote/comfyui/jobs/jobTypes.ts, src/platform/remote/comfyui/jobs/fetchJobs.ts
Adds JobsPageRequest (offset/after), JobsApiError, and FetchHistoryPageResult.nextCursor; Jobs API schema adds next_cursor field.
Pagination API implementation
src/platform/remote/comfyui/jobs/fetchJobs.ts, src/platform/remote/comfyui/jobs/fetchJobs.test.ts
fetchJobsRaw builds queries from after or offset, throws JobsApiError on non-OK responses, and returns nextCursor; fetchHistoryPage accepts page: JobsPageRequest; fetchHistory and fetchQueue call with matching signatures; tests validate cursor mode, error handling, and null-cursor mapping.
Store pagination state and helpers
src/stores/assetsStore.ts
Adds historyNextCursor state, epoch guarding for stale in-flight suppression, and merge/trim helpers for cursor-aware page loading with deduplication and cursor-recovery fallback.
Store head refresh mechanics
src/stores/assetsStore.ts
Implements replaceHistoryWithHeadPage, coalescing refreshHistoryHead with trailing refresh queue, and doRefreshHistoryHead that merges newly completed jobs or replaces state to avoid unreachable gaps; exposes refreshHistoryHead() as a store action; prevents stale continuations via epoch tracking.
Caller integration: UI and missing-media
src/views/GraphView.vue, src/components/queue/QueueProgressOverlay.vue, src/platform/missingMedia/missingMediaAssetResolver.ts, src/platform/missingMedia/missingMediaScan.test.ts, src/views/GraphView.test.ts
Components now call refreshHistoryHead() instead of updateHistory(); missing-media callers pass pagination as { offset }; test mocks updated to match new call shapes and include refreshHistoryHead.
Store tests: migration and comprehensive coverage
src/stores/assetsStore.test.ts
Migrates all history mocking from api.getHistory to fetchHistoryPage with new mockHistoryPage helper; adds extensive cursor pagination test suites covering after/next_cursor progression, offset bootstrap, cursor rejection recovery and fallback, stale in-flight continuation protection, transient error handling, and terminal cases; expands refreshHistoryHead scenarios including full reload, cursor-preserving prepends, error recording with data preservation, head-to-loaded-items reachability, burst coalescing, and server-side deletion pruning; maintains comprehensive error handling, memory management, preview updates, metadata enrichment, and cover image selection tests.
CI workflow: Cursor review automation
.github/workflows/pr-cursor-review.yaml
Adds label-triggered GitHub Actions workflow that runs SHA-pinned reusable Cursor review for PRs labeled cursor-review, overrides diff_excludes, forwards secrets, and applies PR+label concurrency cancellation.

Sequence Diagram

sequenceDiagram
    participant Component as GraphView/Queue
    participant Store as assetsStore
    participant API as fetchHistoryPage
    participant Jobs as /jobs API

    Component->>Store: refreshHistoryHead()
    Store->>API: { offset: 0 }
    API->>Jobs: GET /jobs?offset=0
    Jobs-->>API: { items, has_more, next_cursor }
    API-->>Store: FetchHistoryPageResult (items, hasMore, nextCursor)
    Store->>Store: mergeHistoryAssets or replaceHistoryWithHeadPage
    Store-->>Component: updated history

    Note over Store: loadMoreHistory uses cursor when available
    Component->>Store: loadMoreHistory()
    alt nextCursor available
        Store->>API: { after: nextCursor }
    else fallback to offset
        Store->>API: { offset: lastOffset }
    end
    API->>Jobs: GET /jobs?after=cursor or offset=N
    Jobs-->>API: { items, has_more, next_cursor }
    API-->>Store: FetchHistoryPageResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Cursors hop through history's stack, so spry,
New heads refreshed beneath a watcher's eye.
Epochs guard the hops from stale reprise,
Offset fallback keeps the flow alive!
A rabbit cheers: "Pagination, multiply!"

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adopting cursor pagination in the Generated tab's jobs walk, which aligns with the substantial refactoring across multiple files.
Description check ✅ Passed The PR description comprehensively covers all required template sections with detailed explanations of what changed, design decisions, and implications, significantly exceeding baseline requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
End-To-End Regression Coverage For Fixes ✅ Passed PR title uses "feat" (feature) not bug-fix language; check requires bug-fix signals to trigger. This is a feature adoption, not a bug fix.
Adr Compliance For Entity/Litegraph Changes ✅ Passed PR changes do not modify files under src/lib/litegraph/, src/ecs/, or graph entity files. Check scope does not apply.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/fe-962-fe-adopt-cursor-pagination-in-the-generated-tab-jobs-sourced

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

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
src/platform/remote/comfyui/jobs/fetchJobs.ts (1)

68-70: 💤 Low value

Consider including status text in error message for easier debugging.

Adding res.statusText would provide more context when investigating failures (e.g., distinguishing "400 Bad Request" from "400 INVALID_CURSOR").

♻️ Suggested improvement
   if (!res.ok) {
-    throw new Error(`[Jobs API] Failed to fetch jobs: ${res.status}`)
+    throw new Error(`[Jobs API] Failed to fetch jobs: ${res.status} ${res.statusText}`)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/remote/comfyui/jobs/fetchJobs.ts` around lines 68 - 70, Update
the error thrown on non-OK responses in the fetchJobs flow so it includes both
res.status and res.statusText for better context; locate the check that throws
new Error(`[Jobs API] Failed to fetch jobs: ${res.status}`) (in
src/platform/remote/comfyui/jobs/fetchJobs.ts / the fetchJobs handler) and
change the message to include res.statusText (e.g., combine status and
statusText into the thrown Error message) so failures show both numeric code and
human-readable status.
src/views/GraphView.test.ts (1)

103-107: ⚡ Quick win

Add behavioral assertions for the new history-refresh path.

This mock shape update is necessary, but the suite still doesn’t verify that
status / execution_success flows call assetsStore.refreshHistoryHead()
under the assets-open or linear-mode conditions introduced in GraphView.vue.
Please add focused event-path tests so this integration can’t regress silently.

As per coding guidelines, src/**/*.test.ts should “write tests for all
changes, especially bug fixes to catch future regressions” and aim for
behavioral coverage of critical/new features.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/views/GraphView.test.ts` around lines 103 - 107, The test suite currently
mocks useAssetsStore but lacks assertions that the new history-refresh code path
in GraphView.vue calls assetsStore.refreshHistoryHead; add focused behavioral
tests in GraphView.test.ts that simulate the two event flows (status change and
execution_success) under the two relevant conditions (assets-open and
linear-mode) and assert that the mocked refreshHistoryHead() is called (and not
called when conditions are false). Locate and extend the existing mocked
useAssetsStore (updateHistory and refreshHistoryHead) and add tests that trigger
the component events or lifecycle/state changes used by GraphView (e.g.,
emitting the status/execution_success events or toggling the
assets-open/linear-mode flags) and verify refreshHistoryHead was invoked the
expected number of times.

Source: Coding guidelines

src/stores/assetsStore.test.ts (1)

30-47: 💤 Low value

Consider using a function declaration for the helper.

Per coding guidelines, prefer function declarations over function expressions for pure functions. This helper could be declared as:

-const mockHistoryPage = (
-  jobs: JobListItem[],
-  {
-    hasMore = false,
-    nextCursor,
-    offset = 0,
-    total = jobs.length,
-    limit = 200
-  }: Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {}
-): FetchHistoryPageResult => ({
-  jobs,
-  total,
-  offset,
-  limit,
-  hasMore,
-  nextCursor
-})
+function mockHistoryPage(
+  jobs: JobListItem[],
+  {
+    hasMore = false,
+    nextCursor,
+    offset = 0,
+    total = jobs.length,
+    limit = 200
+  }: Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {}
+): FetchHistoryPageResult {
+  return {
+    jobs,
+    total,
+    offset,
+    limit,
+    hasMore,
+    nextCursor
+  }
+}

Based on learnings: "Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/assetsStore.test.ts` around lines 30 - 47, The helper is defined
as a const arrow function (mockHistoryPage) but the repo prefers pure function
declarations; change the arrow function to a named function declaration
(function mockHistoryPage(jobs: JobListItem[], opts:
Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {}) : FetchHistoryPageResult)
preserving the same parameter defaults and return structure (jobs, total,
offset, limit, hasMore, nextCursor) and keep types JobListItem and
FetchHistoryPageResult intact so callers/tests remain unchanged.

Source: Learnings

src/stores/assetsStore.ts (2)

157-178: 💤 Low value

Consider binary search for sorted insertion.

The current implementation uses findIndex (O(n)) for each insertion, resulting in O(n·m) complexity. Since the list maintains sorted order by created_at, a binary search would reduce this to O(m·log n). Additionally, the repeated new Date() calls inside findIndex create many short-lived objects.

This is acceptable given the MAX_HISTORY_ITEMS=1000 bound, but could be optimized if refresh latency becomes noticeable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/assetsStore.ts` around lines 157 - 178, mergeHistoryAssets
currently uses findIndex with repeated new Date() calls, giving O(n·m) behavior
and many short-lived Date objects; replace the linear search with a binary
search over allHistoryItems.value (which is sorted by created_at) to compute
insertIndex in O(log n), and convert each asset.created_at and existing
item.created_at to timestamps once (e.g., const assetTime = +new
Date(asset.created_at ?? 0)) to avoid repeated Date construction; keep skipping
duplicates via loadedIds and maintain the same push/splice logic (use the
computed insertIndex === -1 meaning append) and respect MAX_HISTORY_ITEMS if
applicable.

335-335: Fix: job.id matches asset.id for this overlap check

loadedIds is populated with AssetItem.id from mapHistoryToAssets, and mapTaskOutputToAssetItem sets id: taskItem.jobId; TaskItemImpl.jobId is derived directly from job.id, so page.jobs.some(job => loadedIds.has(job.id)) compares the same id space.

The only reason reachesLoadedItems could be false despite overlap is filtering: mapHistoryToAssets only adds completed jobs that have preview_output, while page.jobs includes other terminal statuses/jobs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/assetsStore.ts` at line 335, reachesLoadedItems is false due to
mapHistoryToAssets only adding AssetItems for completed jobs with
preview_output, so loadedIds lacks IDs for other terminal jobs present in
page.jobs; update mapHistoryToAssets (and/or mapTaskOutputToAssetItem where
AssetItem.id is set) to also include terminal jobs from history even if they
lack preview_output (or otherwise ensure loadedIds includes job.id for those
terminal statuses), so page.jobs.some(job => loadedIds.has(job.id)) correctly
detects overlap with TaskItemImpl.jobId/job.id.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/platform/remote/comfyui/jobs/fetchJobs.ts`:
- Around line 68-70: Update the error thrown on non-OK responses in the
fetchJobs flow so it includes both res.status and res.statusText for better
context; locate the check that throws new Error(`[Jobs API] Failed to fetch
jobs: ${res.status}`) (in src/platform/remote/comfyui/jobs/fetchJobs.ts / the
fetchJobs handler) and change the message to include res.statusText (e.g.,
combine status and statusText into the thrown Error message) so failures show
both numeric code and human-readable status.

In `@src/stores/assetsStore.test.ts`:
- Around line 30-47: The helper is defined as a const arrow function
(mockHistoryPage) but the repo prefers pure function declarations; change the
arrow function to a named function declaration (function mockHistoryPage(jobs:
JobListItem[], opts: Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {}) :
FetchHistoryPageResult) preserving the same parameter defaults and return
structure (jobs, total, offset, limit, hasMore, nextCursor) and keep types
JobListItem and FetchHistoryPageResult intact so callers/tests remain unchanged.

In `@src/stores/assetsStore.ts`:
- Around line 157-178: mergeHistoryAssets currently uses findIndex with repeated
new Date() calls, giving O(n·m) behavior and many short-lived Date objects;
replace the linear search with a binary search over allHistoryItems.value (which
is sorted by created_at) to compute insertIndex in O(log n), and convert each
asset.created_at and existing item.created_at to timestamps once (e.g., const
assetTime = +new Date(asset.created_at ?? 0)) to avoid repeated Date
construction; keep skipping duplicates via loadedIds and maintain the same
push/splice logic (use the computed insertIndex === -1 meaning append) and
respect MAX_HISTORY_ITEMS if applicable.
- Line 335: reachesLoadedItems is false due to mapHistoryToAssets only adding
AssetItems for completed jobs with preview_output, so loadedIds lacks IDs for
other terminal jobs present in page.jobs; update mapHistoryToAssets (and/or
mapTaskOutputToAssetItem where AssetItem.id is set) to also include terminal
jobs from history even if they lack preview_output (or otherwise ensure
loadedIds includes job.id for those terminal statuses), so page.jobs.some(job =>
loadedIds.has(job.id)) correctly detects overlap with TaskItemImpl.jobId/job.id.

In `@src/views/GraphView.test.ts`:
- Around line 103-107: The test suite currently mocks useAssetsStore but lacks
assertions that the new history-refresh code path in GraphView.vue calls
assetsStore.refreshHistoryHead; add focused behavioral tests in
GraphView.test.ts that simulate the two event flows (status change and
execution_success) under the two relevant conditions (assets-open and
linear-mode) and assert that the mocked refreshHistoryHead() is called (and not
called when conditions are false). Locate and extend the existing mocked
useAssetsStore (updateHistory and refreshHistoryHead) and add tests that trigger
the component events or lifecycle/state changes used by GraphView (e.g.,
emitting the status/execution_success events or toggling the
assets-open/linear-mode flags) and verify refreshHistoryHead was invoked the
expected number of times.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0a3e3d99-e00b-4a26-b511-1783242969cf

📥 Commits

Reviewing files that changed from the base of the PR and between 25205c0 and 17966b1.

📒 Files selected for processing (11)
  • src/components/queue/QueueProgressOverlay.vue
  • src/platform/missingMedia/missingMediaAssetResolver.test.ts
  • src/platform/missingMedia/missingMediaAssetResolver.ts
  • src/platform/missingMedia/missingMediaScan.test.ts
  • src/platform/remote/comfyui/jobs/fetchJobs.test.ts
  • src/platform/remote/comfyui/jobs/fetchJobs.ts
  • src/platform/remote/comfyui/jobs/jobTypes.ts
  • src/stores/assetsStore.test.ts
  • src/stores/assetsStore.ts
  • src/views/GraphView.test.ts
  • src/views/GraphView.vue

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 10, 2026
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.50000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/views/GraphView.vue 0.00% 2 Missing ⚠️
src/components/queue/QueueProgressOverlay.vue 0.00% 1 Missing ⚠️
@@            Coverage Diff             @@
##             main   #12769      +/-   ##
==========================================
+ Coverage   76.08%   76.10%   +0.02%     
==========================================
  Files        1574     1574              
  Lines      101628   101701      +73     
  Branches    31718    31119     -599     
==========================================
+ Hits        77319    77399      +80     
+ Misses      23483    23476       -7     
  Partials      826      826              
Flag Coverage Δ
unit 63.20% <97.50%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...platform/missingMedia/missingMediaAssetResolver.ts 95.45% <ø> (ø)
src/platform/remote/comfyui/jobs/fetchJobs.ts 95.09% <100.00%> (+0.25%) ⬆️
src/platform/remote/comfyui/jobs/jobTypes.ts 100.00% <ø> (ø)
src/stores/assetsStore.ts 69.43% <100.00%> (+4.39%) ⬆️
src/components/queue/QueueProgressOverlay.vue 54.05% <0.00%> (ø)
src/views/GraphView.vue 73.15% <0.00%> (-1.35%) ⬇️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

mattmillerai added a commit that referenced this pull request Jun 10, 2026
Applies verified findings from multi-model review of #12769:

- next_cursor accepts null (z.string().nullish()) so a backend serializing
  absent-as-null can't crash the whole history fetch at the zod boundary
- JobsApiError carries the HTTP status and response body; the store's
  cursor recovery now drops the cursor for the offset fallback only on a
  400 (stale/rejected cursor) and only while its walk is still current —
  transient failures and superseded continuations propagate so a valid
  cursor is never discarded
- an empty page without a cursor is terminal (offset paging cannot
  advance by zero), while an empty page that still mints a cursor keeps
  walking
- refreshHistoryHead coalesces event bursts into the in-flight refresh
  plus exactly one trailing refresh, so a job completing mid-refresh is
  picked up instead of being satisfied by a response dispatched before
  it existed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
src/stores/assetsStore.ts (2)

372-377: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Detect overlap from walked history rows, not only mapped asset IDs.

Line 372 compares raw job.id values against loadedIds, but loadedIds is filled only after mapHistoryToAssets() drops non-preview history rows. If the refreshed head page reaches the already-walked portion through a filtered-out job, reachesLoadedItems stays false and Lines 373-377 unnecessarily fall back to updateHistory(), which resets the user's scroll position even though the page already overlaps the loaded window. Track a separate set of walked history job IDs for this check instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/assetsStore.ts` around lines 372 - 377, The current overlap check
uses loadedIds (populated after mapHistoryToAssets filters out non-preview rows)
so reachesLoadedItems can be false when the refreshed page overlaps
already-walked rows that were filtered; introduce and maintain a separate set
(e.g., walkedHistoryIds) of raw history job IDs as you walk history, use that
set instead of loadedIds when computing reachesLoadedItems in the block
containing the existing reachesLoadedItems / updateHistory logic, and ensure
walkedHistoryIds is kept in sync whenever history is advanced or reset so the
overlap detection correctly avoids unnecessary calls to updateHistory().

373-378: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid clearing the current walk before the restart succeeds.

Line 377 calls updateHistory(), and that path clears historyOffset, historyNextCursor, allHistoryItems, and loadedIds before the fetch runs. If this restart hits a transient error, the old historyAssets list stays visible but the internal pagination state is already gone, so the next loadMoreHistory() starts again at offset 0 and can collapse the list back to the first page. Keep the existing walk state until the replacement fetch succeeds, or restore it on failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/assetsStore.ts` around lines 373 - 378, The current restart flow
in updateHistory() clears historyOffset, historyNextCursor, allHistoryItems, and
loadedIds immediately, which can leave UI state inconsistent if the replacement
fetch fails; change updateHistory()/the restart path so it first performs the
fetch into local/temp variables (e.g.,
tempOffset/tempCursor/tempAllHistoryItems/tempLoadedIds) without mutating the
store, and only replace the store fields (historyOffset, historyNextCursor,
allHistoryItems, loadedIds) after the fetch succeeds; on fetch failure, leave
the existing store state intact (or explicitly roll back to the previous values)
so loadMoreHistory() won't restart from an empty pagination state unexpectedly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/stores/assetsStore.ts`:
- Around line 372-377: The current overlap check uses loadedIds (populated after
mapHistoryToAssets filters out non-preview rows) so reachesLoadedItems can be
false when the refreshed page overlaps already-walked rows that were filtered;
introduce and maintain a separate set (e.g., walkedHistoryIds) of raw history
job IDs as you walk history, use that set instead of loadedIds when computing
reachesLoadedItems in the block containing the existing reachesLoadedItems /
updateHistory logic, and ensure walkedHistoryIds is kept in sync whenever
history is advanced or reset so the overlap detection correctly avoids
unnecessary calls to updateHistory().
- Around line 373-378: The current restart flow in updateHistory() clears
historyOffset, historyNextCursor, allHistoryItems, and loadedIds immediately,
which can leave UI state inconsistent if the replacement fetch fails; change
updateHistory()/the restart path so it first performs the fetch into local/temp
variables (e.g., tempOffset/tempCursor/tempAllHistoryItems/tempLoadedIds)
without mutating the store, and only replace the store fields (historyOffset,
historyNextCursor, allHistoryItems, loadedIds) after the fetch succeeds; on
fetch failure, leave the existing store state intact (or explicitly roll back to
the previous values) so loadMoreHistory() won't restart from an empty pagination
state unexpectedly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2a718eb-f3b2-40c5-9574-1b85d825c6f6

📥 Commits

Reviewing files that changed from the base of the PR and between 17966b1 and 5e95fcc.

📒 Files selected for processing (5)
  • src/platform/remote/comfyui/jobs/fetchJobs.test.ts
  • src/platform/remote/comfyui/jobs/fetchJobs.ts
  • src/platform/remote/comfyui/jobs/jobTypes.ts
  • src/stores/assetsStore.test.ts
  • src/stores/assetsStore.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/platform/remote/comfyui/jobs/jobTypes.ts
  • src/platform/remote/comfyui/jobs/fetchJobs.test.ts
  • src/platform/remote/comfyui/jobs/fetchJobs.ts
  • src/stores/assetsStore.test.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 10, 2026
Calls the reusable Comfy-Org/github-workflows cursor-review.yml (single source of truth for panel, judge, prompts, scripts) instead of a standalone copy. Label-gated to the team; secret-bearing jobs skip fork PRs. Judge overridden to Opus 4.8.

@dante01yoon dante01yoon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Read-only review at 5e95fcc (not approving, per request). This is the careful one, and overall it's well-reasoned: the epoch counter invalidating superseded walks, the coalescing single-trailing refreshHistoryHead, the gap-restart-vs-whole-timeline-replace decisions, and the contained throw-propagation are all solid and well-tested. I verified the "Breaking" change is actually contained — api.getHistory/api.getQueue both still try/catch and return empty, and the store's updateHistory/loadMoreHistory wrappers catch as well.

Two findings inline: one issue: (a stuck-cursor guard the sibling #12774 has but this walk lacks) and one question: on the OSS missing-media behavior change.

Heads-up: your open #12464 (FE-740) and #12398 (FE-746) also touch assetsStore.ts / missingMediaAssetResolver.ts — no duplicate logic, but plan for a rebase whichever lands second.

Comment thread src/stores/assetsStore.ts Outdated
Comment thread src/platform/missingMedia/missingMediaAssetResolver.ts
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 16, 2026
@mattmillerai mattmillerai requested a review from dante01yoon June 16, 2026 20:48

@dante01yoon dante01yoon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at 7e95419

  • issue (stuck-cursor guard) — fixed in 2264e4f. The history jobs walk now mirrors the flat-output walk's non-advancing-cursor guard, and I confirmed it actually terminates in production (loadMoreHistory early-returns on !hasMoreHistory) with a genuine red-green regression test. Thread replied.
  • question (OSS missing-media throw) — confirmed intentional; the throw is load-bearing for cursor recovery (400 INVALID_CURSOR → drop cursor vs transient → retry), and the cloud-branch symmetry is the point. Accepted. Thread replied.

No new issues in the delta since 5e95fcc (the rest is a rebase onto main). CI green on this head (39 success / 9 skipped). Threads left open for you to resolve.

@dante01yoon dante01yoon removed their assignment Jun 17, 2026
dante01yoon
dante01yoon previously approved these changes Jun 17, 2026
Comment thread src/stores/assetsStore.ts Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 17, 2026
@mattmillerai mattmillerai added needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch cloud/1.45 Backport PRs for cloud 1.45 labels Jun 17, 2026
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Assigned @DrJKL or @christian-byrne as second approver prior to merging as discussed.

@DrJKL DrJKL left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review of the cursor-pagination changes. Findings are inline: one correctness issue in the rejected-cursor fallback, one simplification on the head-refresh coalescing, plus minor type-safety, schema, comment, and test items (with suggestions where applicable).

Comment thread src/stores/assetsStore.ts Outdated
Comment thread src/stores/assetsStore.ts
Comment thread src/platform/remote/comfyui/jobs/jobTypes.ts Outdated
Comment thread src/platform/remote/comfyui/jobs/fetchJobs.ts Outdated
Comment thread src/stores/assetsStore.ts Outdated
Comment thread src/stores/assetsStore.ts Outdated
Comment thread src/platform/remote/comfyui/jobs/fetchJobs.test.ts Outdated
Co-authored-by: Alexander Brown <drjkl@comfy.org>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 22, 2026
Co-authored-by: Alexander Brown <drjkl@comfy.org>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 22, 2026
Co-authored-by: Alexander Brown <drjkl@comfy.org>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 22, 2026
Co-authored-by: Alexander Brown <drjkl@comfy.org>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 22, 2026
When a pagination cursor is rejected, fall back to offset 0 and replace
the loaded list rather than using the stale client-side offset. The
previous offset drifted when items were deleted server-side, causing the
recovery page to skip or duplicate rows.

Acceptable tradeoff: this rare fallback resets scroll position.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 22, 2026
# Conflicts:
#	src/stores/assetsStore.test.ts
Comment thread src/stores/assetsStore.ts
return
}

if (page.hasMore) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: in offset-fallback mode (cursorless backends, the Playwright fixture), this merge branch prepends new head jobs but never advances historyOffset. The server timeline shifts down by the number of new completions, so the next offset-mode loadMoreHistory() re-requests an overlapping window, reintroducing the offset drift this PR removes (rows past the shifted window get silently skipped). Before this PR, completion did a full updateHistory() reset and did not drift. Could the offset branch advance historyOffset by the merged-new count, or force a full reset when historyNextCursor == null?

Comment thread src/stores/assetsStore.ts
loadedIds.clear()
newAssets.forEach((asset) => loadedIds.add(asset.id))
historyOffset.value = page.jobs.length
historyNextCursor.value = page.nextCursor ?? null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): every status / execution_success event re-fetches the full head page (BATCH_SIZE 200) and runs mapHistoryToAssets over all 200 jobs (a TaskItemImpl + parseTaskOutput each, then a full sort) to surface ~1 new row. During a batch run this fires per finished job. Since the merge already dedupes by loadedIds, filtering page.jobs to non-seen ids before mapping/parsing would cut steady-state cost from O(p log p) to O(new rows).

Comment thread src/stores/assetsStore.ts
* @param loadMore - true for pagination (append), false for initial load (replace)
* Insert assets in sorted order (newest first), skipping already-loaded ids
*/
const mergeHistoryAssets = (newAssets: AssetItem[]) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): per-item findIndex + splice here is O(page x n), and the predicate re-parses new Date(item.created_at) on the same items repeatedly. Both the incoming page and allHistoryItems are sorted newest-first, so a two-pointer merge (O(page + n)) plus caching getTime() once would remove the repeated parsing on the scroll hot path.

Comment thread src/stores/assetsStore.ts
}
loadedIds.add(asset.id)

const assetTime = new Date(asset.created_at ?? 0).getTime()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: assetTime = new Date(asset.created_at ?? 0).getTime(). If a completed job has a null created_at, assetTime is 0, findIndex returns -1, and the newest job is pushed to the bottom (and may be trimmed away) instead of prepended. Can a terminal job ever lack created_at? If so, treating missing as newest would fix it.

Comment thread src/stores/assetsStore.ts
return
}

historyError.value = null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): refreshHistoryHead is fire-and-forget from completion events and shares historyError with loadMoreHistory with no lock. An interleaved head refresh that clears historyError on entry can hide a real load-more error (or vice versa). A separate error ref for the background head refresh would avoid the cross-talk.

'prompt_1',
'prompt_2'
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): the transient-error tests assert only historyError instanceof Error. Since the code branches on JobsApiError.status === 400 for cursor recovery, asserting toBeInstanceOf(JobsApiError) and .status === 500 here would actually guard the recover-vs-not classification rather than just confirming some error was stored.

@@ -604,7 +1345,9 @@ describe('assetsStore - Refactored (Option A)', () => {
const mockHistory = Array.from({ length: 3 }, (_, i) =>
createMockJobItem(i)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): two boundary gaps in the refresh suite. (1) No test that a refreshHistoryHead merge pushing past MAX_HISTORY_ITEMS trims correctly and evicts from loadedIds (only the offset loadMore path covers trim today). (2) This whole-timeline-replace test asserts only the resulting id list, not that pagination state (cursor / offset / hasMoreHistory) was reset by replaceHistoryWithHeadPage.

@@ -98,7 +125,7 @@ export async function fetchHistory(
maxItems: number = 200,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): fetchHistory keeps a positional offset param while its sibling fetchHistoryPage moved to { offset?, after? }. The two now have divergent, easily-confused pagination signatures. Optionally align fetchHistory to the same options object.

Comment thread src/stores/assetsStore.ts
const history = await api.getHistory(BATCH_SIZE, {
offset: historyOffset.value
})
const epoch = historyFetchEpoch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): the historyFetchEpoch guard discards superseded results but does not abort the request, so the 200-job fetch + zod validation still run to completion on rapid resets. Threading an AbortSignal through fetchHistoryPage -> fetchApi would cancel rather than just ignore the orphaned work.

Comment thread src/stores/assetsStore.ts
historyError.value = err
// Keep existing data when error occurs (consistent with updateHistory)
if (!historyAssets.value.length) {
historyAssets.value = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): historyAssets and allHistoryItems are aliased to the same array (historyAssets.value = allHistoryItems.value), so reactivity relies on the in-place .push / .splice in mergeHistoryAssets. A future edit swapping those for .filter() / spread would silently break propagation. A computed(() => allHistoryItems.value) (or a comment marking the deliberate alias) would harden this.

@christian-byrne christian-byrne removed their assignment Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cloud/1.45 Backport PRs for cloud 1.45 cursor-review Trigger multi-model Cursor agent review on this PR needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants