diff --git a/apps/frontend/src/main/insights-service.ts b/apps/frontend/src/main/insights-service.ts index 43f47751..726cfcbb 100644 --- a/apps/frontend/src/main/insights-service.ts +++ b/apps/frontend/src/main/insights-service.ts @@ -175,7 +175,7 @@ export class InsightsService extends EventEmitter { role: 'assistant', content: result.fullResponse, timestamp: new Date(), - suggestedTask: result.suggestedTask, + suggestedTasks: result.suggestedTasks, toolsUsed: result.toolsUsed.length > 0 ? result.toolsUsed : undefined }; diff --git a/apps/frontend/src/main/insights/insights-executor.ts b/apps/frontend/src/main/insights/insights-executor.ts index 6630b4b1..1153be9b 100644 --- a/apps/frontend/src/main/insights/insights-executor.ts +++ b/apps/frontend/src/main/insights/insights-executor.ts @@ -19,7 +19,7 @@ import { detectRateLimit, createSDKRateLimitInfo } from '../rate-limit-detector' */ interface ProcessorResult { fullResponse: string; - suggestedTask?: InsightsChatMessage['suggestedTask']; + suggestedTasks?: InsightsChatMessage['suggestedTasks']; toolsUsed: InsightsToolUsage[]; } @@ -127,7 +127,7 @@ export class InsightsExecutor extends EventEmitter { return new Promise((resolve, reject) => { let fullResponse = ''; - let suggestedTask: InsightsChatMessage['suggestedTask'] | undefined; + const suggestedTasks: InsightsChatMessage['suggestedTasks'] = []; const toolsUsed: InsightsToolUsage[] = []; let allInsightsOutput = ''; let stderrOutput = ''; @@ -142,7 +142,9 @@ export class InsightsExecutor extends EventEmitter { for (const line of lines) { if (line.startsWith('__TASK_SUGGESTION__:')) { this.handleTaskSuggestion(projectId, line, (task) => { - suggestedTask = task; + if (task) { + suggestedTasks.push(task); + } }); } else if (line.startsWith('__TOOL_START__:')) { this.handleToolStart(projectId, line, toolsUsed); @@ -194,7 +196,7 @@ export class InsightsExecutor extends EventEmitter { resolve({ fullResponse: fullResponse.trim(), - suggestedTask, + suggestedTasks: suggestedTasks.length > 0 ? suggestedTasks : undefined, toolsUsed }); } else { @@ -237,7 +239,7 @@ export class InsightsExecutor extends EventEmitter { private handleTaskSuggestion( projectId: string, line: string, - onTaskFound: (task: InsightsChatMessage['suggestedTask']) => void + onTaskFound: (task: NonNullable[number]) => void ): void { try { const taskJson = line.substring('__TASK_SUGGESTION__:'.length); @@ -245,7 +247,7 @@ export class InsightsExecutor extends EventEmitter { onTaskFound(suggestedTask); this.emit('stream-chunk', projectId, { type: 'task_suggestion', - suggestedTask + suggestedTasks: [suggestedTask] } as InsightsStreamChunk); } catch { // Not valid JSON, treat as normal text (should not emit here as it's already handled) diff --git a/apps/frontend/src/renderer/components/Insights.tsx b/apps/frontend/src/renderer/components/Insights.tsx index 16adcb68..ec5e1e61 100644 --- a/apps/frontend/src/renderer/components/Insights.tsx +++ b/apps/frontend/src/renderer/components/Insights.tsx @@ -39,7 +39,7 @@ import { import { loadTasks } from '../stores/task-store'; import { ChatHistorySidebar } from './ChatHistorySidebar'; import { InsightsModelSelector } from './InsightsModelSelector'; -import type { InsightsChatMessage, InsightsModelConfig } from '../../shared/types'; +import type { InsightsChatMessage, InsightsModelConfig, TaskMetadata } from '../../shared/types'; import { TASK_CATEGORY_LABELS, TASK_CATEGORY_COLORS, @@ -102,7 +102,7 @@ export function Insights({ projectId }: InsightsProps) { }), [t]); const [inputValue, setInputValue] = useState(''); - const [creatingTask, setCreatingTask] = useState(null); + const [creatingTask, setCreatingTask] = useState>(new Set()); const [taskCreated, setTaskCreated] = useState>(new Set()); const [showSidebar, setShowSidebar] = useState(true); const [isUserAtBottom, setIsUserAtBottom] = useState(true); @@ -157,10 +157,11 @@ export function Insights({ projectId }: InsightsProps) { textareaRef.current?.focus(); }, []); - // Reset taskCreated when switching sessions + // Reset task creation state when switching sessions useEffect(() => { setTaskCreated(new Set()); - }, []); + setCreatingTask(new Set()); + }, [session?.id]); const handleSend = () => { const message = inputValue.trim(); @@ -198,25 +199,32 @@ export function Insights({ projectId }: InsightsProps) { return await renameSession(projectId, sessionId, newTitle); }; - const handleCreateTask = async (message: InsightsChatMessage) => { - if (!message.suggestedTask) return; - - setCreatingTask(message.id); + const handleCreateTask = async ( + messageId: string, + taskIndex: number, + taskData: { title: string; description: string; metadata?: TaskMetadata } + ) => { + const taskKey = `${messageId}-${taskIndex}`; + setCreatingTask(prev => new Set(prev).add(taskKey)); try { const task = await createTaskFromSuggestion( projectId, - message.suggestedTask.title, - message.suggestedTask.description, - message.suggestedTask.metadata + taskData.title, + taskData.description, + taskData.metadata ); if (task) { - setTaskCreated(prev => new Set(prev).add(message.id)); + setTaskCreated(prev => new Set(prev).add(taskKey)); // Reload tasks to show the new task in the kanban loadTasks(projectId); } } finally { - setCreatingTask(null); + setCreatingTask(prev => { + const next = new Set(prev); + next.delete(taskKey); + return next; + }); } }; @@ -336,9 +344,9 @@ export function Insights({ projectId }: InsightsProps) { key={message.id} message={message} markdownComponents={markdownComponents} - onCreateTask={() => handleCreateTask(message)} - isCreatingTask={creatingTask === message.id} - taskCreated={taskCreated.has(message.id)} + onCreateTask={handleCreateTask} + creatingTask={creatingTask} + taskCreated={taskCreated} /> ))} @@ -428,18 +436,19 @@ export function Insights({ projectId }: InsightsProps) { interface MessageBubbleProps { message: InsightsChatMessage; markdownComponents: Components; - onCreateTask: () => void; - isCreatingTask: boolean; - taskCreated: boolean; + onCreateTask: (messageId: string, taskIndex: number, taskData: { title: string; description: string; metadata?: TaskMetadata }) => void; + creatingTask: Set; + taskCreated: Set; } function MessageBubble({ message, markdownComponents, onCreateTask, - isCreatingTask, + creatingTask, taskCreated }: MessageBubbleProps) { + const { t } = useTranslation('common'); const isUser = message.role === 'user'; return ( @@ -471,74 +480,84 @@ function MessageBubble({ )} - {/* Task suggestion card */} - {message.suggestedTask && ( - - -
- - - Suggested Task - -
-

- {message.suggestedTask.title} -

-

- {message.suggestedTask.description} -

- {message.suggestedTask.metadata && ( -
- {message.suggestedTask.metadata.category && ( - 0 && ( +
+ {message.suggestedTasks.map((task, index) => { + const taskKey = `${message.id}-${index}`; + const isCreating = creatingTask.has(taskKey); + const isCreated = taskCreated.has(taskKey); + + return ( + + +
+ + + {t('insights.suggestedTask')} + +
+

+ {task.title} +

+

+ {task.description} +

+ {task.metadata && ( +
+ {task.metadata.category && ( + + {TASK_CATEGORY_LABELS[task.metadata.category] || + task.metadata.category} + + )} + {task.metadata.complexity && ( + + {TASK_COMPLEXITY_LABELS[task.metadata.complexity] || + task.metadata.complexity} + + )} +
+ )} +
- )} - - - + + + + ); + })} +
)} diff --git a/apps/frontend/src/renderer/stores/insights-store.ts b/apps/frontend/src/renderer/stores/insights-store.ts index 05df0e90..445647b9 100644 --- a/apps/frontend/src/renderer/stores/insights-store.ts +++ b/apps/frontend/src/renderer/stores/insights-store.ts @@ -23,6 +23,7 @@ interface InsightsState { status: InsightsChatStatus; pendingMessage: string; streamingContent: string; // Accumulates streaming response + streamingTasks: NonNullable; // Accumulates task suggestions during streaming currentTool: ToolUsage | null; // Currently executing tool toolsUsed: InsightsToolUsage[]; // Tools used during current response isLoadingSessions: boolean; @@ -39,7 +40,8 @@ interface InsightsState { setCurrentTool: (tool: ToolUsage | null) => void; addToolUsage: (tool: ToolUsage) => void; clearToolsUsed: () => void; - finalizeStreamingMessage: (suggestedTask?: InsightsChatMessage['suggestedTask']) => void; + addStreamingTasks: (tasks: NonNullable) => void; + finalizeStreamingMessage: () => void; clearSession: () => void; setLoadingSessions: (loading: boolean) => void; } @@ -56,6 +58,7 @@ export const useInsightsStore = create((set, _get) => ({ status: initialStatus, pendingMessage: '', streamingContent: '', + streamingTasks: [], currentTool: null, toolsUsed: [], isLoadingSessions: false, @@ -121,7 +124,7 @@ export const useInsightsStore = create((set, _get) => ({ streamingContent: state.streamingContent + content })), - clearStreamingContent: () => set({ streamingContent: '' }), + clearStreamingContent: () => set({ streamingContent: '', streamingTasks: [] }), setCurrentTool: (tool) => set({ currentTool: tool }), @@ -139,13 +142,19 @@ export const useInsightsStore = create((set, _get) => ({ clearToolsUsed: () => set({ toolsUsed: [] }), - finalizeStreamingMessage: (suggestedTask) => + addStreamingTasks: (tasks) => + set((state) => ({ + streamingTasks: [...state.streamingTasks, ...tasks] + })), + + finalizeStreamingMessage: () => set((state) => { const content = state.streamingContent; const toolsUsed = state.toolsUsed.length > 0 ? [...state.toolsUsed] : undefined; + const suggestedTasks = state.streamingTasks.length > 0 ? [...state.streamingTasks] : undefined; - if (!content && !suggestedTask && !toolsUsed) { - return { streamingContent: '', toolsUsed: [] }; + if (!content && !suggestedTasks && !toolsUsed) { + return { streamingContent: '', streamingTasks: [], toolsUsed: [] }; } const newMessage: InsightsChatMessage = { @@ -153,13 +162,14 @@ export const useInsightsStore = create((set, _get) => ({ role: 'assistant', content, timestamp: new Date(), - suggestedTask, + suggestedTasks, toolsUsed }; if (!state.session) { return { streamingContent: '', + streamingTasks: [], toolsUsed: [], session: { id: `session-${Date.now()}`, @@ -173,6 +183,7 @@ export const useInsightsStore = create((set, _get) => ({ return { streamingContent: '', + streamingTasks: [], toolsUsed: [], session: { ...state.session, @@ -188,6 +199,7 @@ export const useInsightsStore = create((set, _get) => ({ status: initialStatus, pendingMessage: '', streamingContent: '', + streamingTasks: [], currentTool: null, toolsUsed: [] }) @@ -378,9 +390,11 @@ export function setupInsightsListeners(): () => void { store().setCurrentTool(null); break; case 'task_suggestion': - // Finalize the message with task suggestion + // Accumulate task suggestions — they'll be included when 'done' finalizes the message store().setCurrentTool(null); - store().finalizeStreamingMessage(chunk.suggestedTask); + if (chunk.suggestedTasks) { + store().addStreamingTasks(chunk.suggestedTasks); + } break; case 'done': // Finalize any remaining content diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index 57b60ad2..52d380b9 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -419,6 +419,12 @@ "failedLabel": "Failed", "starting": "Starting..." }, + "insights": { + "suggestedTask": "Suggested Task", + "creating": "Creating...", + "taskCreated": "Task Created", + "createTask": "Create Task" + }, "ideation": { "converting": "Converting...", "convertToTask": "Convert to Auto-Build Task", diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index a41527ec..ca99cc40 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -419,6 +419,12 @@ "failedLabel": "Échoué", "starting": "Démarrage..." }, + "insights": { + "suggestedTask": "Tâche suggérée", + "creating": "Création...", + "taskCreated": "Tâche créée", + "createTask": "Créer une tâche" + }, "ideation": { "converting": "Conversion...", "convertToTask": "Convertir en tâche Auto-Build", diff --git a/apps/frontend/src/shared/types/insights.ts b/apps/frontend/src/shared/types/insights.ts index 5d77e13f..ba5d83a5 100644 --- a/apps/frontend/src/shared/types/insights.ts +++ b/apps/frontend/src/shared/types/insights.ts @@ -181,11 +181,11 @@ export interface InsightsChatMessage { content: string; timestamp: Date; // For assistant messages that suggest task creation - suggestedTask?: { + suggestedTasks?: Array<{ title: string; description: string; metadata?: TaskMetadata; - }; + }>; // Tools used during this response (assistant messages only) toolsUsed?: InsightsToolUsage[]; } @@ -220,11 +220,11 @@ export interface InsightsChatStatus { export interface InsightsStreamChunk { type: 'text' | 'task_suggestion' | 'tool_start' | 'tool_end' | 'done' | 'error'; content?: string; - suggestedTask?: { + suggestedTasks?: Array<{ title: string; description: string; metadata?: TaskMetadata; - }; + }>; tool?: { name: string; input?: string; // Brief description of what's being searched/read