diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 835a9ba26a6..bcd96ae0810 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -3835,7 +3835,8 @@ export default function TaskPage(): React.JSX.Element { workItemsInvalidationNonce, taskRefreshNonce, taskSource, - githubMode + githubMode, + taskResumeApplied ]) // Why: the dialog's "Use" button routes through the same direct-launch flow as the row-level "Use" CTA so behavior is consistent regardless of entry point. @@ -6432,7 +6433,11 @@ export default function TaskPage(): React.JSX.Element { githubPerRepoPageLimit ).then(({ totalPages: countedPages }) => { if (!cancelled) { - setCountedTotalPages(countedPages) + // Why: min against an already-applied clamp — the count must not + // re-advertise pages a probe has proven unreachable this generation. + setCountedTotalPages((previous) => + previous !== null && previous > 0 ? Math.min(previous, countedPages) : countedPages + ) } }) diff --git a/src/renderer/src/components/task-page-work-item-pagination.test.ts b/src/renderer/src/components/task-page-work-item-pagination.test.ts index 52bc565f581..c459c9b028f 100644 --- a/src/renderer/src/components/task-page-work-item-pagination.test.ts +++ b/src/renderer/src/components/task-page-work-item-pagination.test.ts @@ -80,10 +80,10 @@ describe('resolveEmptyPageOutcome', () => { ).toEqual({ reason: 'window-unreachable', clampTotalPagesTo: 33 }) }) - it('resolves as load-failed when a window 422 coincides with a thrown repo fetch', () => { - // Repos advance independently; another repo's transient failure says - // nothing about the healthy repos' remaining pages — and the transient - // failure is the actionable signal, so it wins the toast too. + it('resolves as load-failed when a window 422 coincides with a sibling failure', () => { + // Repos advance independently; another repo's failure — thrown or on the + // envelope channel — says nothing about the healthy repos' remaining + // pages, and the transient failure is the actionable signal. expect( resolveEmptyPageOutcome({ ...base, @@ -92,6 +92,13 @@ describe('resolveEmptyPageOutcome', () => { issueErrorTypes: ['validation_error'] }) ).toEqual({ reason: 'load-failed', clampTotalPagesTo: null }) + expect( + resolveEmptyPageOutcome({ + ...base, + target: 5, + issueErrorTypes: ['validation_error', 'permission_denied'] + }) + ).toEqual({ reason: 'load-failed', clampTotalPagesTo: null }) }) it('reports a failure without clamping for transient or unclassified errors', () => { diff --git a/src/renderer/src/components/task-page-work-item-pagination.ts b/src/renderer/src/components/task-page-work-item-pagination.ts index 89a90f36b47..d032f7e028d 100644 --- a/src/renderer/src/components/task-page-work-item-pagination.ts +++ b/src/renderer/src/components/task-page-work-item-pagination.ts @@ -65,7 +65,13 @@ export function resolveEmptyPageOutcome(args: { countedTotalPages: number | null }): EmptyPageOutcome { const clamp = Math.max(1, Math.floor(args.target)) - if (args.issueErrorTypes.includes('validation_error') && args.failedCount === 0) { + // Why: every error must be the window 422 — a sibling repo's envelope + // 403/404 alongside it means a repo that may still have pages, so the + // transient-failure branch must win the toast and block the clamp. + const onlyWindowErrors = + args.issueErrorTypes.length > 0 && + args.issueErrorTypes.every((type) => type === 'validation_error') + if (onlyWindowErrors && args.failedCount === 0) { return { reason: 'window-unreachable', clampTotalPagesTo: clamp } } if (args.failedCount > 0 || args.issueErrorTypes.length > 0) { diff --git a/src/renderer/src/store/slices/github.test.ts b/src/renderer/src/store/slices/github.test.ts index 5706e776c39..f2a56e12c0e 100644 --- a/src/renderer/src/store/slices/github.test.ts +++ b/src/renderer/src/store/slices/github.test.ts @@ -6694,6 +6694,37 @@ describe('createGitHubSlice.fetchWorkItems source/error envelope', () => { expect(result).toEqual({ items: [], failedCount: 0, issueErrorTypes: ['validation_error'] }) }) + it('demotes non-window validation errors so they cannot drive the unreachable clamp', async () => { + runtimeEnvironmentCall.mockResolvedValueOnce({ + id: 'rpc-work-items-page-422-other', + ok: true, + result: { + items: [], + sources: { + issues: { owner: 'up', repo: 'r' }, + prs: null, + originCandidate: { owner: 'up', repo: 'r' }, + upstreamCandidate: null + }, + errors: { + issues: { type: 'validation_error', message: 'Validation Failed: query is malformed' } + } + }, + _meta: { runtimeId: 'remote-runtime' } + }) + const store = createTestStore() + store.setState({ + settings: { activeRuntimeEnvironmentId: 'env-1' }, + repos: [{ id: 'runtime-repo-id', path: '/server/repo', name: 'repo', kind: 'git' }] + } as unknown as Partial) + + const result = await store + .getState() + .fetchWorkItemsNextPage([{ repoId: 'caller-repo-id', path: '/server/repo' }], 24, 100, '', 2) + + expect(result).toEqual({ items: [], failedCount: 0, issueErrorTypes: ['unknown'] }) + }) + it('routes work-item counts through the active runtime environment', async () => { runtimeEnvironmentCall.mockResolvedValueOnce({ id: 'rpc-work-items-count', diff --git a/src/renderer/src/store/slices/github.ts b/src/renderer/src/store/slices/github.ts index d6ae3b0d0fe..6f5d2efb7c7 100644 --- a/src/renderer/src/store/slices/github.ts +++ b/src/renderer/src/store/slices/github.ts @@ -2821,7 +2821,14 @@ export const createGitHubSlice: StateCreator = (s }) // Why: page-N failures aren't in the per-repo banner (keyed on the initial fetch); log them so pagination failures are observable instead of silently truncating (richer surface deferred, design doc §6). if (envelope.errors?.issues) { - issueErrorTypes.push(envelope.errors.issues.type) + const { type, message } = envelope.errors.issues + // Why: only the 1000-result-window 422 may drive the unreachable + // clamp; demote other validation errors so they read as failures. + issueErrorTypes.push( + type === 'validation_error' && !/first 1000 search results/i.test(message) + ? 'unknown' + : type + ) console.warn( `[workItems] next page ${r.repoId} issues-side partial failure:`, envelope.errors.issues