feat: add GitHub issue comment selection and fix auto-start bug

## Features
- Add comment selection UI when creating tasks from GitHub issues
  - Fetch and display all comments with author, timestamp, and preview
  - Allow users to select/deselect individual comments via checkboxes
  - Include "Select All" / "Deselect All" toggle functionality
  - Show selected comment count (e.g., "3/5 comments selected")
  - Only selected comments are included in the task description

- Fix auto-start bug where GitHub issues were immediately executed
  - Tasks now stay in "backlog" status after creation
  - Users must manually start tasks from the Kanban board

## Implementation Details

### Backend
- Added `getIssueComments` IPC handler to fetch comments separately
- Modified `investigateGitHubIssue` to accept selectedCommentIds parameter
- Updated comment filtering logic in buildIssueContext
- Removed automatic startSpecCreation call to prevent auto-execution

### Frontend
- Enhanced InvestigationDialog with scrollable comment list UI
- Updated GitHubAPI interface with getIssueComments method
- Modified hooks and stores to pass selected comment IDs through the chain
- Added projectId prop to InvestigationDialog for comment fetching

### Files Changed
- Backend handlers: investigation-handlers.ts, issue-handlers.ts, types.ts
- API layer: github-api.ts
- UI components: InvestigationDialog.tsx, GitHubIssues.tsx
- State management: github-store.ts, useGitHubInvestigation.ts
- Type definitions: types/index.ts, ipc.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AndyMik90
2025-12-18 18:27:01 +01:00
parent d93eefe806
commit 4c1dd89840
10 changed files with 195 additions and 37 deletions
@@ -66,7 +66,7 @@ export function registerInvestigateIssue(
): void {
ipcMain.on(
IPC_CHANNELS.GITHUB_INVESTIGATE_ISSUE,
async (_, projectId: string, issueNumber: number) => {
async (_, projectId: string, issueNumber: number, selectedCommentIds?: number[]) => {
const mainWindow = getMainWindow();
if (!mainWindow) return;
@@ -104,11 +104,16 @@ export function registerInvestigateIssue(
};
// Fetch issue comments for more context
const comments = await githubFetch(
const allComments = await githubFetch(
config.token,
`/repos/${config.repo}/issues/${issueNumber}/comments`
) as GitHubAPIComment[];
// Filter comments based on selection (if provided)
const comments = selectedCommentIds && selectedCommentIds.length > 0
? allComments.filter(c => selectedCommentIds.includes(c.id))
: allComments;
// Build context for the AI investigation
const labels = issue.labels.map(l => l.name);
const issueContext = buildIssueContext(
@@ -144,14 +149,9 @@ export function registerInvestigateIssue(
issue.html_url
);
// Start spec creation with the existing spec directory
agentManager.startSpecCreation(
specData.specId,
project.path,
specData.taskDescription,
specData.specDir,
specData.metadata
);
// NOTE: We intentionally do NOT call agentManager.startSpecCreation() here
// This allows the task to stay in "backlog" status until the user manually starts it
// Previously, calling startSpecCreation would auto-start the task immediately
// Phase 3: Creating task
sendProgress(mainWindow, projectId, {
@@ -7,7 +7,7 @@ import { IPC_CHANNELS } from '../../../shared/constants';
import type { IPCResult, GitHubIssue } from '../../../shared/types';
import { projectStore } from '../../project-store';
import { getGitHubConfig, githubFetch } from './utils';
import type { GitHubAPIIssue } from './types';
import type { GitHubAPIIssue, GitHubAPIComment } from './types';
/**
* Transform GitHub API issue to application format
@@ -116,10 +116,45 @@ export function registerGetIssue(): void {
);
}
/**
* Get comments for a specific issue
*/
export function registerGetIssueComments(): void {
ipcMain.handle(
IPC_CHANNELS.GITHUB_GET_ISSUE_COMMENTS,
async (_, projectId: string, issueNumber: number): Promise<IPCResult<GitHubAPIComment[]>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
const config = getGitHubConfig(project);
if (!config) {
return { success: false, error: 'No GitHub token or repository configured' };
}
try {
const comments = await githubFetch(
config.token,
`/repos/${config.repo}/issues/${issueNumber}/comments`
) as GitHubAPIComment[];
return { success: true, data: comments };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch issue comments'
};
}
}
);
}
/**
* Register all issue-related handlers
*/
export function registerIssueHandlers(): void {
registerGetIssues();
registerGetIssue();
registerGetIssueComments();
}
@@ -38,8 +38,11 @@ export interface GitHubAPIRepository {
}
export interface GitHubAPIComment {
id: number;
body: string;
user: { login: string };
user: { login: string; avatar_url?: string };
created_at: string;
updated_at: string;
}
export interface ReleaseOptions {
@@ -19,8 +19,9 @@ export interface GitHubAPI {
getGitHubRepositories: (projectId: string) => Promise<IPCResult<GitHubRepository[]>>;
getGitHubIssues: (projectId: string, state?: 'open' | 'closed' | 'all') => Promise<IPCResult<GitHubIssue[]>>;
getGitHubIssue: (projectId: string, issueNumber: number) => Promise<IPCResult<GitHubIssue>>;
getIssueComments: (projectId: string, issueNumber: number) => Promise<IPCResult<any[]>>;
checkGitHubConnection: (projectId: string) => Promise<IPCResult<GitHubSyncStatus>>;
investigateGitHubIssue: (projectId: string, issueNumber: number) => void;
investigateGitHubIssue: (projectId: string, issueNumber: number, selectedCommentIds?: number[]) => void;
importGitHubIssues: (projectId: string, issueNumbers: number[]) => Promise<IPCResult<GitHubImportResult>>;
createGitHubRelease: (
projectId: string,
@@ -66,11 +67,14 @@ export const createGitHubAPI = (): GitHubAPI => ({
getGitHubIssue: (projectId: string, issueNumber: number): Promise<IPCResult<GitHubIssue>> =>
invokeIpc(IPC_CHANNELS.GITHUB_GET_ISSUE, projectId, issueNumber),
getIssueComments: (projectId: string, issueNumber: number): Promise<IPCResult<any[]>> =>
invokeIpc(IPC_CHANNELS.GITHUB_GET_ISSUE_COMMENTS, projectId, issueNumber),
checkGitHubConnection: (projectId: string): Promise<IPCResult<GitHubSyncStatus>> =>
invokeIpc(IPC_CHANNELS.GITHUB_CHECK_CONNECTION, projectId),
investigateGitHubIssue: (projectId: string, issueNumber: number): void =>
sendIpc(IPC_CHANNELS.GITHUB_INVESTIGATE_ISSUE, projectId, issueNumber),
investigateGitHubIssue: (projectId: string, issueNumber: number, selectedCommentIds?: number[]): void =>
sendIpc(IPC_CHANNELS.GITHUB_INVESTIGATE_ISSUE, projectId, issueNumber, selectedCommentIds),
importGitHubIssues: (projectId: string, issueNumbers: number[]): Promise<IPCResult<GitHubImportResult>> =>
invokeIpc(IPC_CHANNELS.GITHUB_IMPORT_ISSUES, projectId, issueNumbers),
@@ -48,9 +48,9 @@ export function GitHubIssues({ onOpenSettings }: GitHubIssuesProps) {
setShowInvestigateDialog(true);
}, []);
const handleStartInvestigation = useCallback(() => {
const handleStartInvestigation = useCallback((selectedCommentIds: number[]) => {
if (selectedIssueForInvestigation) {
startInvestigation(selectedIssueForInvestigation);
startInvestigation(selectedIssueForInvestigation, selectedCommentIds);
}
}, [selectedIssueForInvestigation, startInvestigation]);
@@ -125,6 +125,7 @@ export function GitHubIssues({ onOpenSettings }: GitHubIssuesProps) {
investigationStatus={investigationStatus}
onStartInvestigation={handleStartInvestigation}
onClose={handleCloseDialog}
projectId={selectedProject?.id}
/>
</div>
);
@@ -1,6 +1,9 @@
import { Sparkles, Loader2, CheckCircle2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Sparkles, Loader2, CheckCircle2, MessageCircle } from 'lucide-react';
import { Button } from '../../ui/button';
import { Progress } from '../../ui/progress';
import { Checkbox } from '../../ui/checkbox';
import { ScrollArea } from '../../ui/scroll-area';
import {
Dialog,
DialogContent,
@@ -10,6 +13,15 @@ import {
DialogTitle
} from '../../ui/dialog';
import type { InvestigationDialogProps } from '../types';
import { formatDate } from '../utils';
interface GitHubComment {
id: number;
body: string;
user: { login: string; avatar_url?: string };
created_at: string;
updated_at: string;
}
export function InvestigationDialog({
open,
@@ -17,11 +29,60 @@ export function InvestigationDialog({
selectedIssue,
investigationStatus,
onStartInvestigation,
onClose
onClose,
projectId
}: InvestigationDialogProps) {
const [comments, setComments] = useState<GitHubComment[]>([]);
const [selectedCommentIds, setSelectedCommentIds] = useState<number[]>([]);
const [loadingComments, setLoadingComments] = useState(false);
// Fetch comments when dialog opens
useEffect(() => {
if (open && selectedIssue && projectId) {
setLoadingComments(true);
setComments([]);
setSelectedCommentIds([]);
window.electronAPI.getIssueComments(projectId, selectedIssue.number)
.then((result) => {
if (result.success && result.data) {
setComments(result.data);
// By default, select all comments
setSelectedCommentIds(result.data.map(c => c.id));
}
})
.catch((err) => {
console.error('Failed to fetch comments:', err);
})
.finally(() => {
setLoadingComments(false);
});
}
}, [open, selectedIssue, projectId]);
const toggleComment = (commentId: number) => {
setSelectedCommentIds(prev =>
prev.includes(commentId)
? prev.filter(id => id !== commentId)
: [...prev, commentId]
);
};
const toggleAllComments = () => {
if (selectedCommentIds.length === comments.length) {
setSelectedCommentIds([]);
} else {
setSelectedCommentIds(comments.map(c => c.id));
}
};
const handleStartInvestigation = () => {
onStartInvestigation(selectedCommentIds);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-info" />
@@ -37,19 +98,71 @@ export function InvestigationDialog({
</DialogHeader>
{investigationStatus.phase === 'idle' ? (
<div className="space-y-4">
<div className="space-y-4 flex-1 min-h-0 flex flex-col">
<p className="text-sm text-muted-foreground">
Create a task from this GitHub issue. The task will be added to your Kanban board in the Planned column.
Create a task from this GitHub issue. The task will be added to your Kanban board in the Backlog column.
</p>
<div className="rounded-lg border border-border bg-muted/30 p-4">
<h4 className="text-sm font-medium mb-2">The task will include:</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Issue title and description</li>
<li> Link back to the GitHub issue</li>
<li> Labels and metadata from the issue</li>
<li> Ready to start when you move it to In Progress</li>
</ul>
</div>
{/* Comments section */}
{loadingComments ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : comments.length > 0 ? (
<div className="space-y-2 flex-1 min-h-0 flex flex-col">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium flex items-center gap-2">
<MessageCircle className="h-4 w-4" />
Select Comments to Include ({selectedCommentIds.length}/{comments.length})
</h4>
<Button
variant="ghost"
size="sm"
onClick={toggleAllComments}
className="text-xs"
>
{selectedCommentIds.length === comments.length ? 'Deselect All' : 'Select All'}
</Button>
</div>
<ScrollArea className="flex-1 min-h-0 border rounded-md">
<div className="p-2 space-y-2">
{comments.map((comment) => (
<div
key={comment.id}
className="flex gap-3 p-3 rounded-lg border border-border bg-card hover:bg-accent/50 transition-colors cursor-pointer"
onClick={() => toggleComment(comment.id)}
>
<Checkbox
checked={selectedCommentIds.includes(comment.id)}
onCheckedChange={() => toggleComment(comment.id)}
onClick={(e) => e.stopPropagation()}
/>
<div className="flex-1 space-y-1 min-w-0">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-medium">{comment.user.login}</span>
<span></span>
<span>{formatDate(comment.created_at)}</span>
</div>
<p className="text-sm text-foreground whitespace-pre-wrap break-words line-clamp-3">
{comment.body}
</p>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
) : (
<div className="rounded-lg border border-border bg-muted/30 p-4">
<h4 className="text-sm font-medium mb-2">The task will include:</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Issue title and description</li>
<li> Link back to the GitHub issue</li>
<li> Labels and metadata from the issue</li>
<li> No comments (this issue has no comments)</li>
</ul>
</div>
)}
</div>
) : (
<div className="space-y-4">
@@ -82,7 +195,7 @@ export function InvestigationDialog({
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={onStartInvestigation}>
<Button onClick={handleStartInvestigation}>
<Sparkles className="h-4 w-4 mr-2" />
Create Task
</Button>
@@ -56,9 +56,9 @@ export function useGitHubInvestigation(projectId: string | undefined) {
};
}, [projectId, setInvestigationStatus, setInvestigationResult, setError]);
const startInvestigation = useCallback((issue: GitHubIssue) => {
const startInvestigation = useCallback((issue: GitHubIssue, selectedCommentIds: number[]) => {
if (projectId) {
investigateGitHubIssue(projectId, issue.number);
investigateGitHubIssue(projectId, issue.number, selectedCommentIds);
}
}, [projectId]);
@@ -29,8 +29,9 @@ export interface InvestigationDialogProps {
message: string;
error?: string;
};
onStartInvestigation: () => void;
onStartInvestigation: (selectedCommentIds: number[]) => void;
onClose: () => void;
projectId?: string;
}
export interface IssueListHeaderProps {
@@ -147,7 +147,7 @@ export async function checkGitHubConnection(projectId: string): Promise<GitHubSy
}
}
export function investigateGitHubIssue(projectId: string, issueNumber: number): void {
export function investigateGitHubIssue(projectId: string, issueNumber: number, selectedCommentIds?: number[]): void {
const store = useGitHubStore.getState();
store.setInvestigationStatus({
phase: 'fetching',
@@ -157,7 +157,7 @@ export function investigateGitHubIssue(projectId: string, issueNumber: number):
});
store.setInvestigationResult(null);
window.electronAPI.investigateGitHubIssue(projectId, issueNumber);
window.electronAPI.investigateGitHubIssue(projectId, issueNumber, selectedCommentIds);
}
export async function importGitHubIssues(
@@ -174,6 +174,7 @@ export const IPC_CHANNELS = {
GITHUB_GET_REPOSITORIES: 'github:getRepositories',
GITHUB_GET_ISSUES: 'github:getIssues',
GITHUB_GET_ISSUE: 'github:getIssue',
GITHUB_GET_ISSUE_COMMENTS: 'github:getIssueComments',
GITHUB_CHECK_CONNECTION: 'github:checkConnection',
GITHUB_INVESTIGATE_ISSUE: 'github:investigateIssue',
GITHUB_IMPORT_ISSUES: 'github:importIssues',