From d7bf73663554566fa4e69a80438c9875fca1aec0 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 12 Jun 2026 19:54:42 -0700 Subject: [PATCH] Scope task retry state by source context Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 1 + src/renderer/src/components/TaskPage.tsx | 62 +++++++++---------- .../task-page-cache-selectors.test.ts | 23 +++++++ .../components/task-page-cache-selectors.ts | 2 + 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 99f1821798f..cc98a9234b3 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -228,3 +228,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-12: Re-verified project-first drag/reorder behavior across workspace manual order, drag units, workspace-board pointer drops, host-section order/rows, repo reorder host splitting, and project-header drag helpers; 9 files / 62 tests passed plus targeted oxlint. - [x] 2026-06-12: Routed Linear and Jira Tasks reads, create flows, list/detail mutations, comments, metadata, and New Workspace handoff through explicit account-backed `TaskSourceContext` while preserving legacy focused-runtime cache keys for old callers; verified with Linear/Jira source-routing regressions, focused Linear/Jira slice tests (2 files / 48 tests), targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. - [x] 2026-06-12: Preserved Linear task detail source context in `openTaskPage` data, back/forward history entries, history de-dupe keys, and Linear warm prefetch so direct detail opens replay the same account/host source instead of recomputing from focused settings; verified with UI/history tests (2 files / 131 tests) and full `pnpm run typecheck`. +- [x] 2026-06-12: Scoped GitHub Tasks partial-failure retry ownership to the selected source cache scope instead of `repoPath`, so same-project/same-path multi-host failures cannot make the wrong source banner look in-flight; verified with `task-page-cache-selectors.test.ts`, targeted oxlint, and full `pnpm run typecheck`. diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 292c580309a..323b634554f 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -3256,33 +3256,33 @@ export default function TaskPage(): React.JSX.Element { // Why: on a partial-failure retry the cache still holds successful-side // data, so `tasksLoading` (which is gated on `anyUncached`) never flips // true and the Retry button would otherwise give no feedback. Track - // retry-in-flight per repo (keyed by `repoPath`) so that clicking Retry - // on one banner only flips that banner's button into its "Retrying…" + // retry-in-flight per selected source so that clicking Retry + // on one banner only flips that source's button into its "Retrying…" // state — other still-failing banners stay in their "Retry" state rather // than misleadingly flipping in lockstep. The fetch effect clears the set // when the nonce-driven refresh settles. - const [retryingRepoPaths, setRetryingRepoPaths] = useState>(() => new Set()) + const [retryingSourceKeys, setRetryingSourceKeys] = useState>(() => new Set()) const handleRetryIssuesFetch = useCallback( - (repoPath: string) => { - const repo = selectedRepos.find((r) => r.path === repoPath) - if (!repo) { + (sourceKey: string) => { + const source = perRepoSourceState.find((s) => s.sourceKey === sourceKey) + if (!source) { return } // Why: bumping the shared refresh nonce reuses the Tasks list's // single fetch path — nonce changes are treated as force=true so // retry doesn't silently dedupe onto a still-failing in-flight request. // The nonce bump refreshes ALL selected repos, but the Retrying… - // state is scoped to the clicked repo so other banners stay in their + // state is scoped to the clicked source so other banners stay in their // "Retry" state rather than misleadingly flipping to "Retrying…". - setRetryingRepoPaths((prev) => { + setRetryingSourceKeys((prev) => { const next = new Set(prev) - next.add(repoPath) + next.add(source.sourceKey) return next }) setTaskRefreshNonce((n) => n + 1) }, - [selectedRepos] + [perRepoSourceState] ) const handleRefreshGithubTasks = useCallback((): void => { setTasksRefreshing(true) @@ -5267,19 +5267,19 @@ export default function TaskPage(): React.JSX.Element { if (!taskResumeApplied) { return } - // Why: both early-return branches must clear `retryingRepoPaths` — if the + // Why: both early-return branches must clear `retryingSourceKeys` — if the // user clicks Retry and then switches `taskSource` away from 'github' (or // somehow ends up with zero repos selected) before the fetch dispatches, // neither the `.then` nor the `.catch` below will fire, and the Retry // button would stay stuck in its disabled/Retrying state indefinitely. if (taskSource !== 'github' || githubMode !== 'items') { - setRetryingRepoPaths(new Set()) + setRetryingSourceKeys(new Set()) setTasksRefreshing(false) setTasksFiltering(false) return } if (selectedRepos.length === 0) { - setRetryingRepoPaths(new Set()) + setRetryingSourceKeys(new Set()) setTasksRefreshing(false) setTasksFiltering(false) return @@ -5360,29 +5360,29 @@ export default function TaskPage(): React.JSX.Element { // so the toolbar still shows a refresh-in-progress affordance. setTasksRefreshing(forcedFetch) - // Why: snapshot the retrying paths at effect-dispatch so overlapping + // Why: snapshot the retrying source keys at effect-dispatch so overlapping // retries don't clear each other's pending state. An earlier cancelled // effect settling after a newer retry starts would otherwise wipe the - // newer retry's repo from the set. Clearing only the paths captured + // newer retry's source from the set. Clearing only the keys captured // when this effect dispatched preserves later additions. - const dispatchedRetryPaths = retryingRepoPaths + const dispatchedRetrySourceKeys = retryingSourceKeys void fetchWorkItemsAcrossRepos(repoArgs, PER_REPO_FETCH_LIMIT, CROSS_REPO_DISPLAY_LIMIT, q, { ...deriveTaskPageGitHubWorkItemsFetchOptions(forcedFetch, shouldProbeOnLanding) }) .then(({ items, failedCount: failed }) => { - // Why: clear only the repos this effect was responsible for + // Why: clear only the sources this effect was responsible for // retrying (the snapshot captured at dispatch time). Overlapping // retries — a second click while a prior fetch is still in flight - // — must not clear the newer repo from the set, so we can't just + // — must not clear the newer source from the set, so we can't just // reset the whole set here. The early-return branches above reset // the whole set because those branches won't dispatch a fetch. - setRetryingRepoPaths((prev) => { - if (dispatchedRetryPaths.size === 0) { + setRetryingSourceKeys((prev) => { + if (dispatchedRetrySourceKeys.size === 0) { return prev } const next = new Set(prev) - for (const p of dispatchedRetryPaths) { - next.delete(p) + for (const key of dispatchedRetrySourceKeys) { + next.delete(key) } return next }) @@ -5408,19 +5408,19 @@ export default function TaskPage(): React.JSX.Element { .catch((err) => { // Why: fetchWorkItemsAcrossRepos swallows per-repo failures, so a // reject here means an IPC-level or programmer error — surface it. - // Clear only the repos this effect was responsible for retrying + // Clear only the sources this effect was responsible for retrying // (the snapshot captured at dispatch time). Overlapping retries — // a second click while a prior fetch is still in flight — must - // not clear the newer repo from the set, so we can't just reset + // not clear the newer source from the set, so we can't just reset // the whole set here. The early-return branches above reset the // whole set because those branches won't dispatch a fetch. - setRetryingRepoPaths((prev) => { - if (dispatchedRetryPaths.size === 0) { + setRetryingSourceKeys((prev) => { + if (dispatchedRetrySourceKeys.size === 0) { return prev } const next = new Set(prev) - for (const p of dispatchedRetryPaths) { - next.delete(p) + for (const key of dispatchedRetrySourceKeys) { + next.delete(key) } return next }) @@ -8139,10 +8139,10 @@ export default function TaskPage(): React.JSX.Element {