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:
@@ -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>
|
||||
);
|
||||
|
||||
+128
-15
@@ -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>
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user