agent-ecosystem/src/renderer/utils/taskChangeRequest.ts
iliya bec8a6184a fix: refine regex patterns and improve utility functions for mention handling
- Updated regex patterns in chipUtils and mentionLinkify to enhance boundary detection for mentions.
- Refactored taskChangeRequest to simplify earliest date calculation using array destructuring.
- Improved taskReferenceUtils by replacing character boundary checks with a more concise regex.
- Enhanced teamMessageFiltering to ensure boolean checks for message filtering conditions.
- Adjusted urlMatchUtils to refine URL matching regex for better accuracy.
- Updated crossTeam constants to include comments for regex patterns, improving code clarity.
- Removed unused CommentAttachmentPayload type from api.ts to clean up type definitions.
- Introduced McpInstallScope type for better type safety in mcp.ts.
- Enhanced extensionNormalizers to improve URL normalization and added tests for parseGitHubOwnerRepo function.
- Cleaned up pricing.ts by removing unnecessary eslint disable comments.
- Added tests for new functionality in chipUtils and crossTeam constants, ensuring robust coverage.
2026-03-19 13:35:51 +02:00

143 lines
4 KiB
TypeScript

import {
getTaskChangeStateBucket,
isTaskChangeSummaryCacheable,
type TaskChangeStateBucket,
} from '@shared/utils/taskChangeState';
import type { ReviewAPI } from '@shared/types/api';
import type { TeamTaskWithKanban } from '@shared/types/team';
const TASK_SINCE_GRACE_MS = 2 * 60 * 1000;
export type TaskChangeRequestOptions = NonNullable<Parameters<ReviewAPI['getTaskChanges']>[2]>;
export interface TaskChangeContext {
taskId: string;
requestOptions: TaskChangeRequestOptions;
initialFilePath?: string;
}
type TaskChangeTaskLike = Pick<
TeamTaskWithKanban,
| 'id'
| 'owner'
| 'status'
| 'createdAt'
| 'updatedAt'
| 'workIntervals'
| 'historyEvents'
| 'reviewState'
| 'kanbanColumn'
>;
export function deriveTaskSince(task: TaskChangeTaskLike | null): string | undefined {
if (!task) return undefined;
const sources: string[] = [];
if (task.createdAt) sources.push(task.createdAt);
if (Array.isArray(task.workIntervals)) {
for (const interval of task.workIntervals) {
if (interval.startedAt) sources.push(interval.startedAt);
}
}
if (Array.isArray(task.historyEvents)) {
for (const event of task.historyEvents) {
if (event.timestamp) sources.push(event.timestamp);
}
}
if (sources.length === 0) return undefined;
const [first, ...rest] = sources;
const earliest = rest.reduce((a, b) => (a < b ? a : b), first);
const date = new Date(earliest);
date.setTime(date.getTime() - TASK_SINCE_GRACE_MS);
return date.toISOString();
}
export function buildTaskChangeRequestOptions(
task: TaskChangeTaskLike,
overrides?: Partial<TaskChangeRequestOptions>
): TaskChangeRequestOptions {
const options: TaskChangeRequestOptions = {
owner: task.owner,
status: task.status,
intervals: task.workIntervals,
since: deriveTaskSince(task),
stateBucket: getTaskChangeStateBucket(task),
};
return {
...options,
...overrides,
};
}
export function buildTaskChangeContext(
task: TaskChangeTaskLike,
input?: { initialFilePath?: string; summaryOnly?: boolean }
): TaskChangeContext {
return {
taskId: task.id,
requestOptions: buildTaskChangeRequestOptions(task, {
summaryOnly: input?.summaryOnly,
}),
initialFilePath: input?.initialFilePath,
};
}
export function buildTaskChangeSignature(options: TaskChangeRequestOptions): string {
const owner = typeof options.owner === 'string' ? options.owner.trim() : '';
const status = typeof options.status === 'string' ? options.status.trim() : '';
const since = typeof options.since === 'string' ? options.since : '';
const stateBucket = typeof options.stateBucket === 'string' ? options.stateBucket : 'active';
const intervals = Array.isArray(options.intervals)
? options.intervals.map((interval) => ({
startedAt: interval.startedAt,
completedAt: interval.completedAt ?? '',
}))
: [];
return JSON.stringify({
owner,
status,
since,
stateBucket,
intervals,
});
}
export function buildTaskChangePresenceKey(
teamName: string,
taskId: string,
options: TaskChangeRequestOptions
): string {
return `${teamName}:${taskId}:${buildTaskChangeSignature(options)}`;
}
export function getTaskChangeStateBucketFromOptions(
options: TaskChangeRequestOptions | null | undefined
): TaskChangeStateBucket {
switch (options?.stateBucket) {
case 'approved':
case 'review':
case 'completed':
return options.stateBucket;
default:
return 'active';
}
}
export function isTaskSummaryCacheableForOptions(
options: TaskChangeRequestOptions | null | undefined
): boolean {
return isTaskChangeSummaryCacheable(getTaskChangeStateBucketFromOptions(options));
}
export function canDisplayTaskChangesForOptions(
options: TaskChangeRequestOptions | null | undefined
): boolean {
const bucket = getTaskChangeStateBucketFromOptions(options);
if (bucket !== 'active') return true;
// 'active' bucket includes both pending and in_progress — show for in_progress only
return options?.status === 'in_progress';
}