agent-ecosystem/src/main/services/team/taskChangePresenceUtils.ts
iliya 507bf798eb improvement(task-change): improve task change presence tracking and related IPC handlers
- Added support for tracking task change presence with new IPC channels: TEAM_GET_TASK_CHANGE_PRESENCE and TEAM_SET_CHANGE_PRESENCE_TRACKING.
- Introduced JsonTaskChangePresenceRepository and TeamLogSourceTracker to manage task change presence data.
- Enhanced ChangeExtractorService to utilize task change presence services for improved task change detection.
- Updated TeamDataService to integrate task change presence tracking and resolve task change presence states.
- Modified UI components to reflect task change presence status in Kanban and task detail views.

This feature aims to provide real-time insights into task changes, enhancing user experience and task management capabilities.
2026-03-27 17:52:39 +02:00

152 lines
4.5 KiB
TypeScript

import {
getTaskChangeStateBucket,
type TaskChangeStateBucket,
} from '@shared/utils/taskChangeState';
import { createHash } from 'crypto';
export interface TaskChangePresenceInterval {
startedAt: string;
completedAt?: string;
}
export interface TaskChangePresenceDescriptorInput {
owner?: string;
status?: string;
intervals?: TaskChangePresenceInterval[];
since?: string;
reviewState?: 'review' | 'needsFix' | 'approved' | 'none';
historyEvents?: unknown[];
kanbanColumn?: 'review' | 'approved';
}
export interface TaskChangePresenceDescriptor {
stateBucket: TaskChangeStateBucket;
taskSignature: string;
effectiveOptions: {
owner?: string;
status?: string;
intervals?: TaskChangePresenceInterval[];
since?: string;
};
}
function deriveIntervalsFromHistory(
historyEvents?: unknown[]
): TaskChangePresenceInterval[] | undefined {
if (!Array.isArray(historyEvents) || historyEvents.length === 0) {
return undefined;
}
const transitions = historyEvents
.map((event) =>
event && typeof event === 'object' ? (event as Record<string, unknown>) : null
)
.filter((event): event is Record<string, unknown> => event !== null)
.filter((event) => event.type === 'status_changed')
.map((event) => ({
to: typeof event.to === 'string' ? event.to : null,
timestamp: typeof event.timestamp === 'string' ? event.timestamp : null,
}))
.filter(
(transition): transition is { to: string; timestamp: string } =>
transition.to !== null && transition.timestamp !== null
)
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
if (transitions.length === 0) {
return undefined;
}
const derived: TaskChangePresenceInterval[] = [];
let currentStart: string | null = null;
for (const transition of transitions) {
if (transition.to === 'in_progress') {
if (!currentStart) {
currentStart = transition.timestamp;
}
continue;
}
if (currentStart) {
derived.push({ startedAt: currentStart, completedAt: transition.timestamp });
currentStart = null;
}
}
if (currentStart) {
derived.push({ startedAt: currentStart });
}
return derived.length > 0 ? derived : undefined;
}
export function normalizeTaskChangePresenceFilePath(filePath: string): string {
const normalized = filePath.replace(/\\/g, '/');
return normalized.replace(/^[A-Z]:/, (drive) => drive.toLowerCase());
}
export function computeTaskChangePresenceProjectFingerprint(
projectPath?: string | null
): string | null {
const normalizedProjectPath = typeof projectPath === 'string' ? projectPath.trim() : '';
if (!normalizedProjectPath) {
return null;
}
return createHash('sha256')
.update(normalizeTaskChangePresenceFilePath(normalizedProjectPath))
.digest('hex');
}
export function buildTaskChangePresenceDescriptor(
input: TaskChangePresenceDescriptorInput
): TaskChangePresenceDescriptor {
const effectiveIntervals =
Array.isArray(input.intervals) && input.intervals.length > 0
? input.intervals.map((interval) => ({
startedAt: interval.startedAt,
completedAt: interval.completedAt ?? '',
}))
: (deriveIntervalsFromHistory(input.historyEvents)?.map((interval) => ({
startedAt: interval.startedAt,
completedAt: interval.completedAt ?? '',
})) ?? []);
const stateBucket = getTaskChangeStateBucket({
status: input.status,
reviewState: input.reviewState,
historyEvents: input.historyEvents,
kanbanColumn: input.kanbanColumn,
});
const effectiveOptions = {
owner: typeof input.owner === 'string' ? input.owner.trim() : '',
status: typeof input.status === 'string' ? input.status.trim() : '',
intervals: effectiveIntervals,
since: typeof input.since === 'string' ? input.since : '',
};
return {
stateBucket,
taskSignature: JSON.stringify({
owner: effectiveOptions.owner,
status: effectiveOptions.status,
since: effectiveOptions.since,
stateBucket,
intervals: effectiveIntervals,
}),
effectiveOptions: {
owner: effectiveOptions.owner || undefined,
status: effectiveOptions.status || undefined,
intervals:
effectiveIntervals.length > 0
? effectiveIntervals.map((interval) => ({
startedAt: interval.startedAt,
completedAt: interval.completedAt || undefined,
}))
: undefined,
since: effectiveOptions.since || undefined,
},
};
}