- Added new configuration options for task status change notifications, including toggles for enabling notifications, solo mode restrictions, and customizable target statuses. - Updated validation logic to accommodate new notification settings, ensuring proper handling of boolean and array types. - Enhanced UI components to allow users to manage status change notification preferences effectively. - Implemented backend logic to detect and trigger notifications based on task status changes, improving user awareness of task progress.
209 lines
6.5 KiB
TypeScript
209 lines
6.5 KiB
TypeScript
/**
|
|
* ConversationGroupBuilder - Alternative grouping strategy for conversation flow.
|
|
*
|
|
* Groups one user message with all AI responses until the next user message.
|
|
* This is a cleaner alternative to buildChunks() that:
|
|
* - Uses simpler time-based grouping
|
|
* - Separates Task executions from regular tool executions
|
|
* - Links subagents more explicitly via TaskExecution
|
|
*/
|
|
|
|
import {
|
|
type ConversationGroup,
|
|
isParsedUserChunkMessage,
|
|
type ParsedMessage,
|
|
type Process,
|
|
type TaskExecution,
|
|
type ToolCall,
|
|
type ToolExecution,
|
|
} from '@main/types';
|
|
import { calculateMetrics } from '@main/utils/jsonl';
|
|
|
|
/**
|
|
* Build conversation groups using simplified grouping strategy.
|
|
* Groups one user message with all AI responses until the next user message.
|
|
*/
|
|
export function buildGroups(messages: ParsedMessage[], subagents: Process[]): ConversationGroup[] {
|
|
const groups: ConversationGroup[] = [];
|
|
|
|
// Step 1: Filter to main thread only (not sidechain)
|
|
const mainMessages = messages.filter((m) => !m.isSidechain);
|
|
|
|
// Step 2: Find all REAL user messages (these start groups)
|
|
// Use isParsedUserChunkMessage to filter out noise
|
|
const userMessages = mainMessages.filter(isParsedUserChunkMessage);
|
|
|
|
// Step 3: For each user message, collect all AI responses until next user message
|
|
for (let i = 0; i < userMessages.length; i++) {
|
|
const userMsg = userMessages[i];
|
|
const nextUserMsg = userMessages[i + 1];
|
|
|
|
// Collect all messages between this user message and the next
|
|
const aiResponses = collectAIResponses(mainMessages, userMsg, nextUserMsg);
|
|
|
|
// Separate Task tool results from regular tool executions
|
|
const { taskExecutions, regularToolExecutions } = separateTaskExecutions(
|
|
aiResponses,
|
|
subagents
|
|
);
|
|
|
|
// Link subagents to this group via deterministic parentTaskId matching
|
|
const groupSubagents = linkSubagentsToGroup(aiResponses, subagents);
|
|
|
|
// Calculate metrics
|
|
const { startTime, endTime, durationMs } = calculateGroupTiming(userMsg, aiResponses);
|
|
const metrics = calculateMetrics([userMsg, ...aiResponses]);
|
|
|
|
groups.push({
|
|
id: `group-${i + 1}`,
|
|
type: 'user-ai-exchange',
|
|
userMessage: userMsg,
|
|
aiResponses,
|
|
processes: groupSubagents,
|
|
toolExecutions: regularToolExecutions,
|
|
taskExecutions,
|
|
startTime,
|
|
endTime,
|
|
durationMs,
|
|
metrics,
|
|
});
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
/**
|
|
* Collect AI responses between a user message and the next user message.
|
|
* Simpler than collectResponses - just uses timestamp boundaries.
|
|
*/
|
|
function collectAIResponses(
|
|
messages: ParsedMessage[],
|
|
userMsg: ParsedMessage,
|
|
nextUserMsg: ParsedMessage | undefined
|
|
): ParsedMessage[] {
|
|
const responses: ParsedMessage[] = [];
|
|
const startTime = userMsg.timestamp;
|
|
const endTime = nextUserMsg?.timestamp;
|
|
|
|
for (const msg of messages) {
|
|
// Skip if before this user message
|
|
if (msg.timestamp <= startTime) continue;
|
|
|
|
// Skip if at or after next user message
|
|
if (endTime && msg.timestamp >= endTime) continue;
|
|
|
|
// Include ALL non-user messages (assistant + internal user messages)
|
|
if (msg.type === 'assistant' || (msg.type === 'user' && msg.isMeta === true)) {
|
|
responses.push(msg);
|
|
}
|
|
}
|
|
|
|
return responses;
|
|
}
|
|
|
|
/**
|
|
* Separate Task executions from regular tool executions.
|
|
* Task tools spawn subagents, so we track them separately to avoid duplication.
|
|
*/
|
|
function separateTaskExecutions(
|
|
responses: ParsedMessage[],
|
|
allSubagents: Process[]
|
|
): { taskExecutions: TaskExecution[]; regularToolExecutions: ToolExecution[] } {
|
|
const taskExecutions: TaskExecution[] = [];
|
|
const regularToolExecutions: ToolExecution[] = [];
|
|
|
|
// Build map of tool_use_id -> subagent for Task calls
|
|
const taskIdToSubagent = new Map<string, Process>();
|
|
for (const subagent of allSubagents) {
|
|
if (subagent.parentTaskId) {
|
|
taskIdToSubagent.set(subagent.parentTaskId, subagent);
|
|
}
|
|
}
|
|
|
|
// Collect all tool calls
|
|
const toolCalls = new Map<string, { call: ToolCall; timestamp: Date }>();
|
|
for (const msg of responses) {
|
|
if (msg.type === 'assistant') {
|
|
for (const toolCall of msg.toolCalls) {
|
|
toolCalls.set(toolCall.id, { call: toolCall, timestamp: msg.timestamp });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Match with results
|
|
for (const msg of responses) {
|
|
if (msg.type === 'user' && msg.isMeta === true && msg.sourceToolUseID) {
|
|
const callInfo = toolCalls.get(msg.sourceToolUseID);
|
|
if (!callInfo) continue;
|
|
|
|
// Check if this is a Task call with a subagent
|
|
const subagent = taskIdToSubagent.get(msg.sourceToolUseID);
|
|
if (callInfo.call.name === 'Task' && subagent) {
|
|
// This is a Task execution
|
|
taskExecutions.push({
|
|
taskCall: callInfo.call,
|
|
taskCallTimestamp: callInfo.timestamp,
|
|
subagent,
|
|
toolResult: msg,
|
|
resultTimestamp: msg.timestamp,
|
|
durationMs: msg.timestamp.getTime() - callInfo.timestamp.getTime(),
|
|
});
|
|
} else {
|
|
// Regular tool execution
|
|
const result = msg.toolResults[0];
|
|
if (result) {
|
|
regularToolExecutions.push({
|
|
toolCall: callInfo.call,
|
|
result,
|
|
startTime: callInfo.timestamp,
|
|
endTime: msg.timestamp,
|
|
durationMs: msg.timestamp.getTime() - callInfo.timestamp.getTime(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { taskExecutions, regularToolExecutions };
|
|
}
|
|
|
|
/**
|
|
* Link subagents to a conversation group via deterministic parentTaskId matching.
|
|
* Only includes subagents whose parentTaskId matches a Task tool_use ID in the AI responses.
|
|
*/
|
|
function linkSubagentsToGroup(aiResponses: ParsedMessage[], allSubagents: Process[]): Process[] {
|
|
const groupTaskIds = new Set<string>();
|
|
for (const msg of aiResponses) {
|
|
for (const toolCall of msg.toolCalls) {
|
|
if (toolCall.isTask) {
|
|
groupTaskIds.add(toolCall.id);
|
|
}
|
|
}
|
|
}
|
|
return allSubagents
|
|
.filter((s) => s.parentTaskId && groupTaskIds.has(s.parentTaskId))
|
|
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
|
|
}
|
|
|
|
/**
|
|
* Calculate group timing from user message and AI responses.
|
|
*/
|
|
function calculateGroupTiming(
|
|
userMsg: ParsedMessage,
|
|
aiResponses: ParsedMessage[]
|
|
): { startTime: Date; endTime: Date; durationMs: number } {
|
|
const startTime = userMsg.timestamp;
|
|
|
|
let endTime = startTime;
|
|
for (const resp of aiResponses) {
|
|
if (resp.timestamp > endTime) {
|
|
endTime = resp.timestamp;
|
|
}
|
|
}
|
|
|
|
return {
|
|
startTime,
|
|
endTime,
|
|
durationMs: endTime.getTime() - startTime.getTime(),
|
|
};
|
|
}
|