From 9b9f6201795726d9e7f093bfd1f0ae22b409e2bb Mon Sep 17 00:00:00 2001 From: Saad Date: Fri, 16 Jan 2026 17:47:29 -0600 Subject: [PATCH] Prevent duplicate GitHub issue selection Track linked issues per project and disable them in the picker to avoid duplicate work. --- src/renderer/App.tsx | 22 ++++++ .../components/GitHubIssueSelector.tsx | 70 +++++++++++++------ .../components/TaskAdvancedSettings.tsx | 4 ++ src/renderer/components/TaskModal.tsx | 4 ++ src/renderer/types/chat.ts | 6 ++ 5 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 41754166..7dd3bb91 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -48,6 +48,7 @@ import { type Provider } from './types'; import type { Project, Task } from './types/app'; import type { TaskMetadata } from './types/chat'; import { type GitHubIssueSummary } from './types/github'; +import { type GitHubIssueLink } from './types/chat'; import { type JiraIssueSummary } from './types/jira'; import { type LinearIssueSummary } from './types/linear'; @@ -68,6 +69,21 @@ const TERMINAL_PROVIDER_IDS = [ 'rovo', ] as const; +const buildLinkedGithubIssueMap = (tasks?: Task[] | null): Map => { + const linked = new Map(); + if (!tasks?.length) return linked; + for (const task of tasks) { + const issueNumber = task.metadata?.githubIssue?.number; + if (typeof issueNumber !== 'number' || linked.has(issueNumber)) continue; + linked.set(issueNumber, { + number: issueNumber, + taskId: task.id, + taskName: task.name, + }); + } + return linked; +}; + const RightSidebarBridge: React.FC<{ onCollapsedChange: (collapsed: boolean) => void; setCollapsedRef: React.MutableRefObject<((next: boolean) => void) | null>; @@ -1946,6 +1962,11 @@ const AppContent: React.FC = () => { }; }, [handleDeviceFlowSuccess, handleDeviceFlowError, checkStatus]); + const linkedGithubIssueMap = useMemo( + () => buildLinkedGithubIssueMap(selectedProject?.tasks), + [selectedProject?.tasks] + ); + const renderMainContent = () => { if (selectedProject && showKanban) { return ( @@ -2244,6 +2265,7 @@ const AppContent: React.FC = () => { projectName={selectedProject?.name || ''} defaultBranch={projectDefaultBranch} existingNames={(selectedProject?.tasks || []).map((w) => w.name)} + linkedGithubIssueMap={linkedGithubIssueMap} projectPath={selectedProject?.path} branchOptions={projectBranchOptions} isLoadingBranches={isLoadingBranches} diff --git a/src/renderer/components/GitHubIssueSelector.tsx b/src/renderer/components/GitHubIssueSelector.tsx index 815a6f53..1fbad199 100644 --- a/src/renderer/components/GitHubIssueSelector.tsx +++ b/src/renderer/components/GitHubIssueSelector.tsx @@ -7,6 +7,7 @@ import { Separator } from './ui/separator'; import { Badge } from './ui/badge'; import { Spinner } from './ui/spinner'; import { type GitHubIssueSummary } from '../types/github'; +import { type GitHubIssueLink } from '../types/chat'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; import { GitHubIssuePreviewTooltip } from './GitHubIssuePreviewTooltip'; @@ -14,16 +15,22 @@ interface GitHubIssueSelectorProps { projectPath: string; selectedIssue: GitHubIssueSummary | null; onIssueChange: (issue: GitHubIssueSummary | null) => void; + linkedIssueMap?: ReadonlyMap; + linkedIssueMode?: 'disable' | 'hide'; isOpen?: boolean; className?: string; disabled?: boolean; placeholder?: string; } +const EMPTY_LINKED_ISSUE_MAP = new Map(); + export const GitHubIssueSelector: React.FC = ({ projectPath, selectedIssue, onIssueChange, + linkedIssueMap, + linkedIssueMode = 'disable', isOpen = false, className = '', disabled = false, @@ -125,22 +132,29 @@ export const GitHubIssueSelector: React.FC = ({ return availableIssues; }, [searchResults, availableIssues, searchTerm]); + const linkedIssueLookup = linkedIssueMap ?? EMPTY_LINKED_ISSUE_MAP; + + const filteredIssues = useMemo(() => { + if (linkedIssueMode !== 'hide' || linkedIssueLookup.size === 0) return displayIssues; + return displayIssues.filter((issue) => !linkedIssueLookup.has(issue.number)); + }, [displayIssues, linkedIssueLookup, linkedIssueMode]); + useEffect(() => setVisibleCount(10), [searchTerm]); const showIssues = useMemo( - () => displayIssues.slice(0, Math.max(10, visibleCount)), - [displayIssues, visibleCount] + () => filteredIssues.slice(0, Math.max(10, visibleCount)), + [filteredIssues, visibleCount] ); const handleScroll = useCallback( (e: React.UIEvent) => { const el = e.currentTarget; const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 16; - if (nearBottom && showIssues.length < displayIssues.length) { - setVisibleCount((prev) => Math.min(prev + 10, displayIssues.length)); + if (nearBottom && showIssues.length < filteredIssues.length) { + setVisibleCount((prev) => Math.min(prev + 10, filteredIssues.length)); } }, - [displayIssues.length, showIssues.length] + [filteredIssues.length, showIssues.length] ); const handleIssueSelect = (value: string) => { @@ -149,7 +163,7 @@ export const GitHubIssueSelector: React.FC = ({ return; } const num = Number(String(value).replace(/^#/, '')); - const issue = displayIssues.find((i) => i.number === num) ?? null; + const issue = filteredIssues.find((i) => i.number === num) ?? null; onIssueChange(issue); }; @@ -228,23 +242,37 @@ export const GitHubIssueSelector: React.FC = ({ {showIssues.length > 0 ? ( - showIssues.map((issue) => ( - - - - - GitHub - - #{issue.number} + showIssues.map((issue) => { + const linkedIssue = linkedIssueLookup.get(issue.number); + const isLinked = Boolean(linkedIssue); + const isDisabled = linkedIssueMode === 'disable' && isLinked; + return ( + + + + + GitHub + + #{issue.number} + + {issue.title ? ( + {issue.title} + ) : null} + {linkedIssueMode === 'disable' && isLinked ? ( + + In use + + ) : null} - {issue.title ? ( - {issue.title} - ) : null} - - - - )) + + + ); + }) ) : searchTerm.trim() ? (
{isSearching ? ( diff --git a/src/renderer/components/TaskAdvancedSettings.tsx b/src/renderer/components/TaskAdvancedSettings.tsx index fc13d33f..32a4381a 100644 --- a/src/renderer/components/TaskAdvancedSettings.tsx +++ b/src/renderer/components/TaskAdvancedSettings.tsx @@ -14,6 +14,7 @@ import LinearSetupForm from './integrations/LinearSetupForm'; import JiraSetupForm from './integrations/JiraSetupForm'; import { type LinearIssueSummary } from '../types/linear'; import { type GitHubIssueSummary } from '../types/github'; +import { type GitHubIssueLink } from '../types/chat'; import { type JiraIssueSummary } from '../types/jira'; interface TaskAdvancedSettingsProps { @@ -43,6 +44,7 @@ interface TaskAdvancedSettingsProps { // GitHub selectedGithubIssue: GitHubIssueSummary | null; onGithubIssueChange: (issue: GitHubIssueSummary | null) => void; + linkedGithubIssueMap?: ReadonlyMap; isGithubConnected: boolean; onGithubConnect: () => Promise; githubLoading: boolean; @@ -72,6 +74,7 @@ export const TaskAdvancedSettings: React.FC = ({ onLinearConnect, selectedGithubIssue, onGithubIssueChange, + linkedGithubIssueMap, isGithubConnected, onGithubConnect, githubLoading, @@ -324,6 +327,7 @@ export const TaskAdvancedSettings: React.FC = ({ projectPath={projectPath || ''} selectedIssue={selectedGithubIssue} onIssueChange={handleGithubIssueChange} + linkedIssueMap={linkedGithubIssueMap} isOpen={isOpen} disabled={ !hasInitialPromptSupport || diff --git a/src/renderer/components/TaskModal.tsx b/src/renderer/components/TaskModal.tsx index fe2f9506..c044d54a 100644 --- a/src/renderer/components/TaskModal.tsx +++ b/src/renderer/components/TaskModal.tsx @@ -14,6 +14,7 @@ import { providerMeta } from '../providers/meta'; import { isValidProviderId } from '@shared/providers/registry'; import { type LinearIssueSummary } from '../types/linear'; import { type GitHubIssueSummary } from '../types/github'; +import { type GitHubIssueLink } from '../types/chat'; import { type JiraIssueSummary } from '../types/jira'; import { generateFriendlyTaskName, @@ -41,6 +42,7 @@ interface TaskModalProps { projectName: string; defaultBranch: string; existingNames?: string[]; + linkedGithubIssueMap?: ReadonlyMap; projectPath?: string; branchOptions?: BranchOption[]; isLoadingBranches?: boolean; @@ -53,6 +55,7 @@ const TaskModal: React.FC = ({ projectName, defaultBranch, existingNames = [], + linkedGithubIssueMap, projectPath, branchOptions = [], isLoadingBranches = false, @@ -326,6 +329,7 @@ const TaskModal: React.FC = ({ onLinearConnect={integrations.handleLinearConnect} selectedGithubIssue={selectedGithubIssue} onGithubIssueChange={setSelectedGithubIssue} + linkedGithubIssueMap={linkedGithubIssueMap} isGithubConnected={integrations.isGithubConnected} onGithubConnect={integrations.handleGithubConnect} githubLoading={integrations.githubLoading} diff --git a/src/renderer/types/chat.ts b/src/renderer/types/chat.ts index 169b6ad6..250e5493 100644 --- a/src/renderer/types/chat.ts +++ b/src/renderer/types/chat.ts @@ -9,6 +9,12 @@ export interface ProviderRun { runs: number; } +export interface GitHubIssueLink { + number: number; + taskId: string; + taskName: string; +} + export interface TaskMetadata { linearIssue?: LinearIssueSummary | null; githubIssue?: GitHubIssueSummary | null;