fix: resolve 245 eslint errors across 12 files
- rendererLogs: inline IPC channel constants to fix boundary violation - EditorFileWatcher/TaskBoundaryParser: rename UPPER_CASE class props to camelCase - TeamConfigReader: remove unused readFileHead/extractQuotedString functions - TeamFsWorkerClient: use crypto.randomUUID(), remove duplicated branches - team-fs-worker: full type safety rewrite — 10 interfaces replacing ~150 any casts, extract readTasksDirForTeam diagnostics to avoid param reassign - useDiffNavigation/editorSlice/buildSelectionAction: add missing return types - useFileSuggestions: suppress intentional setState-in-effect - store/index: suppress intentional deprecated navigator.platform fallback - platformPath: replace vulnerable regexes with manual loops
This commit is contained in:
parent
2a187894a5
commit
075523dc42
12 changed files with 260 additions and 128 deletions
|
|
@ -1,6 +1,10 @@
|
|||
import { RENDERER_BOOT, RENDERER_HEARTBEAT, RENDERER_LOG } from '@preload/constants/ipcChannels';
|
||||
import { type IpcMain } from 'electron';
|
||||
|
||||
// IPC channel names — must match the preload bindings in src/preload/index.ts
|
||||
const RENDERER_LOG = 'renderer:log';
|
||||
const RENDERER_BOOT = 'renderer:boot';
|
||||
const RENDERER_HEARTBEAT = 'renderer:heartbeat';
|
||||
|
||||
const lastHeartbeatByWebContentsId = new Map<number, number>();
|
||||
const lastHeartbeatWarnedAtByWebContentsId = new Map<number, number>();
|
||||
const hasReceivedHeartbeatByWebContentsId = new Set<number>();
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class EditorFileWatcher {
|
|||
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private onChangeCallback: ((event: EditorFileChangeEvent) => void) | null = null;
|
||||
// Higher debounce = fewer IPC events during large bursts (checkout/build/format).
|
||||
private readonly DEBOUNCE_MS = 350;
|
||||
private readonly debounceMs = 350;
|
||||
private ignoreChangeUntilMs = 0;
|
||||
private watchedFilesKey = '';
|
||||
private watchedDirsKey = '';
|
||||
|
|
@ -229,7 +229,7 @@ export class EditorFileWatcher {
|
|||
emit('delete');
|
||||
if (emitted < MAX_EMITTED_EVENTS_PER_FLUSH) emit('create');
|
||||
if (emitted < MAX_EMITTED_EVENTS_PER_FLUSH) emit('change');
|
||||
}, this.DEBOUNCE_MS);
|
||||
}, this.debounceMs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const TEAMCTL_TASK_REGEX = /task\s+(start|complete|set-status)\s+(\d+)/;
|
|||
|
||||
export class TaskBoundaryParser {
|
||||
private cache = new Map<string, BoundaryCacheEntry>();
|
||||
private readonly CACHE_TTL = 60 * 1000; // 60s
|
||||
private readonly cacheTtl = 60 * 1000; // 60s
|
||||
|
||||
/** Парсинг JSONL файла для обнаружения границ задач */
|
||||
async parseBoundaries(filePath: string): Promise<TaskBoundariesResult> {
|
||||
|
|
@ -127,7 +127,7 @@ export class TaskBoundaryParser {
|
|||
this.cache.set(filePath, {
|
||||
data: result,
|
||||
mtime: fileStat.mtimeMs,
|
||||
expiresAt: Date.now() + this.CACHE_TTL,
|
||||
expiresAt: Date.now() + this.cacheTtl,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,32 +48,6 @@ function withReadTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|||
});
|
||||
}
|
||||
|
||||
async function readFileHead(filePath: string, maxBytes: number): Promise<string> {
|
||||
const handle = await fs.promises.open(filePath, 'r');
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
const bytesToRead = Math.max(0, Math.min(stat.size, maxBytes));
|
||||
if (bytesToRead === 0) return '';
|
||||
const buffer = Buffer.alloc(bytesToRead);
|
||||
await handle.read(buffer, 0, bytesToRead, 0);
|
||||
return buffer.toString('utf8');
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
function extractQuotedString(head: string, key: string): string | null {
|
||||
const re = new RegExp(`"${key}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`);
|
||||
const match = re.exec(head);
|
||||
if (!match?.[1]) return null;
|
||||
try {
|
||||
const value = JSON.parse(match[1]) as unknown;
|
||||
return typeof value === 'string' ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class TeamConfigReader {
|
||||
constructor(
|
||||
private readonly membersMetaStore: TeamMembersMetaStore = new TeamMembersMetaStore()
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ type WorkerResponse =
|
|||
| { id: string; ok: false; error: string };
|
||||
|
||||
function makeId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
return `${Date.now()}-${crypto.randomUUID().slice(0, 12)}`;
|
||||
}
|
||||
|
||||
function resolveWorkerPath(): string | null {
|
||||
|
|
@ -172,11 +172,7 @@ export class TeamFsWorkerClient {
|
|||
reject(error);
|
||||
},
|
||||
});
|
||||
const msg: WorkerRequest =
|
||||
op === 'listTeams'
|
||||
? ({ id, op, payload } as WorkerRequest)
|
||||
: ({ id, op, payload } as WorkerRequest);
|
||||
worker.postMessage(msg);
|
||||
worker.postMessage({ id, op, payload } as WorkerRequest);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,105 @@ type WorkerResponse =
|
|||
| { id: string; ok: true; result: unknown; diag?: unknown }
|
||||
| { id: string; ok: false; error: string };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diagnostic types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SlowEntry {
|
||||
teamName: string;
|
||||
ms: number;
|
||||
}
|
||||
|
||||
interface ListTeamsDiag {
|
||||
op: string;
|
||||
startedAt: number;
|
||||
teamsDir: string;
|
||||
totalDirs: number;
|
||||
returned: number;
|
||||
skipped: number;
|
||||
skipReasons: Record<string, number>;
|
||||
slowest: SlowEntry[];
|
||||
totalMs: number;
|
||||
}
|
||||
|
||||
interface GetAllTasksDiag {
|
||||
op: string;
|
||||
startedAt: number;
|
||||
tasksBase: string;
|
||||
teamDirs: number;
|
||||
returned: number;
|
||||
skipped: number;
|
||||
skipReasons: Record<string, number>;
|
||||
slowestTeams: SlowEntry[];
|
||||
totalMs: number;
|
||||
}
|
||||
|
||||
interface TaskReadDiag {
|
||||
skipped: number;
|
||||
skipReasons: Record<string, number>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parsed JSON types (loose shapes from disk)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ParsedConfig {
|
||||
name?: unknown;
|
||||
description?: unknown;
|
||||
color?: unknown;
|
||||
projectPath?: unknown;
|
||||
leadSessionId?: unknown;
|
||||
deletedAt?: unknown;
|
||||
projectPathHistory?: unknown;
|
||||
sessionHistory?: unknown;
|
||||
members?: unknown;
|
||||
}
|
||||
|
||||
interface RawMember {
|
||||
name?: unknown;
|
||||
agentType?: unknown;
|
||||
role?: unknown;
|
||||
color?: unknown;
|
||||
removedAt?: unknown;
|
||||
}
|
||||
|
||||
interface ParsedTask {
|
||||
id?: unknown;
|
||||
subject?: unknown;
|
||||
title?: unknown;
|
||||
description?: unknown;
|
||||
activeForm?: unknown;
|
||||
owner?: unknown;
|
||||
createdBy?: unknown;
|
||||
status?: unknown;
|
||||
blocks?: unknown;
|
||||
blockedBy?: unknown;
|
||||
related?: unknown;
|
||||
createdAt?: unknown;
|
||||
projectPath?: unknown;
|
||||
comments?: unknown;
|
||||
needsClarification?: unknown;
|
||||
metadata?: { _internal?: unknown };
|
||||
workIntervals?: unknown;
|
||||
}
|
||||
|
||||
interface RawWorkInterval {
|
||||
startedAt?: unknown;
|
||||
completedAt?: unknown;
|
||||
}
|
||||
|
||||
interface RawComment {
|
||||
id?: unknown;
|
||||
author?: unknown;
|
||||
text?: unknown;
|
||||
createdAt?: unknown;
|
||||
type?: unknown;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isAbortError(error: unknown): boolean {
|
||||
return (
|
||||
!!error &&
|
||||
|
|
@ -71,7 +170,7 @@ async function readFileHeadUtf8(filePath: string, maxBytes: number): Promise<str
|
|||
|
||||
function extractQuotedString(head: string, key: string): string | null {
|
||||
const re = new RegExp(`"${key}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`);
|
||||
const match = head.match(re);
|
||||
const match = re.exec(head);
|
||||
if (!match?.[1]) return null;
|
||||
try {
|
||||
const value = JSON.parse(match[1]) as unknown;
|
||||
|
|
@ -104,9 +203,49 @@ function nowMs(): number {
|
|||
return Date.now();
|
||||
}
|
||||
|
||||
async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[]; diag: unknown }> {
|
||||
function bumpSkipReason(reasons: Record<string, number>, reason: string): void {
|
||||
// eslint-disable-next-line no-param-reassign -- accumulator mutation is intentional
|
||||
reasons[reason] = (reasons[reason] || 0) + 1;
|
||||
}
|
||||
|
||||
function pushSlowest(list: SlowEntry[], entry: SlowEntry, maxLen: number): void {
|
||||
list.push(entry);
|
||||
list.sort((a, b) => b.ms - a.ms);
|
||||
// eslint-disable-next-line no-param-reassign -- truncate in-place is intentional
|
||||
if (list.length > maxLen) list.length = maxLen;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// listTeams
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRawMember(v: unknown): v is RawMember {
|
||||
return !!v && typeof v === 'object';
|
||||
}
|
||||
|
||||
function mergeMember(
|
||||
m: RawMember,
|
||||
memberMap: Map<string, { name: string; role?: string; color?: string }>,
|
||||
removedKeys: ReadonlySet<string>
|
||||
): void {
|
||||
const name = typeof m.name === 'string' ? m.name.trim() : '';
|
||||
if (!name) return;
|
||||
if (name === 'team-lead' || name === 'user' || m.agentType === 'team-lead') return;
|
||||
const key = name.toLowerCase();
|
||||
if (removedKeys.has(key)) return;
|
||||
const existing = memberMap.get(key);
|
||||
memberMap.set(key, {
|
||||
name: existing?.name ?? name,
|
||||
role: (typeof m.role === 'string' && m.role.trim()) || existing?.role,
|
||||
color: (typeof m.color === 'string' && m.color.trim()) || existing?.color,
|
||||
});
|
||||
}
|
||||
|
||||
async function listTeams(
|
||||
payload: ListTeamsPayload
|
||||
): Promise<{ teams: unknown[]; diag: ListTeamsDiag }> {
|
||||
const startedAt = nowMs();
|
||||
const diag: any = {
|
||||
const diag: ListTeamsDiag = {
|
||||
op: 'listTeams',
|
||||
startedAt,
|
||||
teamsDir: payload.teamsDir,
|
||||
|
|
@ -136,7 +275,7 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
|
||||
const skip = (reason: string): null => {
|
||||
diag.skipped++;
|
||||
diag.skipReasons[reason] = (diag.skipReasons[reason] || 0) + 1;
|
||||
bumpSkipReason(diag.skipReasons, reason);
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
@ -149,7 +288,7 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
if (!stat.isFile()) return skip('config_not_file');
|
||||
if (stat.size > payload.maxConfigBytes) return skip('config_too_large');
|
||||
|
||||
let config: any = null;
|
||||
let config: ParsedConfig | null = null;
|
||||
let displayName: string | null = null;
|
||||
let description = '';
|
||||
let color: string | undefined;
|
||||
|
|
@ -175,7 +314,7 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
deletedAt = typeof del === 'string' ? del : undefined;
|
||||
} else {
|
||||
const raw = await readFileUtf8WithTimeout(configPath, payload.maxConfigReadMs);
|
||||
config = JSON.parse(raw);
|
||||
config = JSON.parse(raw) as ParsedConfig;
|
||||
displayName = typeof config.name === 'string' ? config.name : null;
|
||||
description = typeof config.description === 'string' ? config.description : '';
|
||||
color =
|
||||
|
|
@ -191,10 +330,10 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
? config.leadSessionId
|
||||
: undefined;
|
||||
projectPathHistory = Array.isArray(config.projectPathHistory)
|
||||
? config.projectPathHistory.slice(-payload.maxProjectPathHistoryInSummary)
|
||||
? (config.projectPathHistory as string[]).slice(-payload.maxProjectPathHistoryInSummary)
|
||||
: undefined;
|
||||
sessionHistory = Array.isArray(config.sessionHistory)
|
||||
? config.sessionHistory.slice(-payload.maxSessionHistoryInSummary)
|
||||
? (config.sessionHistory as string[]).slice(-payload.maxSessionHistoryInSummary)
|
||||
: undefined;
|
||||
deletedAt = typeof config.deletedAt === 'string' ? config.deletedAt : undefined;
|
||||
}
|
||||
|
|
@ -210,41 +349,25 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
|
||||
const memberMap = new Map<string, { name: string; role?: string; color?: string }>();
|
||||
const removedKeys = new Set<string>();
|
||||
const mergeMember = (m: any): void => {
|
||||
const name = typeof m?.name === 'string' ? m.name.trim() : '';
|
||||
if (!name) return;
|
||||
// Summary/memberCount should represent teammates (exclude the lead process).
|
||||
if (name === 'team-lead' || name === 'user' || m?.agentType === 'team-lead') return;
|
||||
const key = name.toLowerCase();
|
||||
// If meta marks this name removed, do not surface it in summaries
|
||||
if (removedKeys.has(key)) return;
|
||||
const existing = memberMap.get(key);
|
||||
memberMap.set(key, {
|
||||
name: existing?.name ?? name,
|
||||
role: (typeof m.role === 'string' && m.role.trim()) || existing?.role,
|
||||
color: (typeof m.color === 'string' && m.color.trim()) || existing?.color,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const metaPath = path.join(payload.teamsDir, teamName, 'members.meta.json');
|
||||
const metaStat = await fs.promises.stat(metaPath);
|
||||
if (metaStat.isFile() && metaStat.size <= payload.maxMembersMetaBytes) {
|
||||
const raw = await readFileUtf8WithTimeout(metaPath, payload.maxConfigReadMs);
|
||||
const parsed = JSON.parse(raw);
|
||||
const members: any[] = Array.isArray(parsed?.members) ? parsed.members : [];
|
||||
const parsed = JSON.parse(raw) as { members?: unknown };
|
||||
const members: unknown[] = Array.isArray(parsed?.members) ? parsed.members : [];
|
||||
for (const member of members) {
|
||||
if (!member || typeof member !== 'object') continue;
|
||||
if (!isRawMember(member)) continue;
|
||||
const name = typeof member.name === 'string' ? member.name.trim() : '';
|
||||
if (!name) continue;
|
||||
// Summary/memberCount should represent teammates (exclude the lead process).
|
||||
if (name === 'team-lead' || member.agentType === 'team-lead') continue;
|
||||
const key = name.toLowerCase();
|
||||
if (member.removedAt) {
|
||||
removedKeys.add(key);
|
||||
continue;
|
||||
}
|
||||
mergeMember(member);
|
||||
mergeMember(member, memberMap, removedKeys);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -253,8 +376,10 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
|
||||
// Merge config members AFTER meta so removedAt can suppress stale config entries.
|
||||
if (config && Array.isArray(config.members)) {
|
||||
for (const member of config.members) {
|
||||
mergeMember(member);
|
||||
for (const member of config.members as unknown[]) {
|
||||
if (isRawMember(member)) {
|
||||
mergeMember(member, memberMap, removedKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,9 +402,7 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
|
||||
const ms = nowMs() - t0;
|
||||
if (ms >= 250) {
|
||||
diag.slowest.push({ teamName, ms });
|
||||
diag.slowest.sort((a: any, b: any) => b.ms - a.ms);
|
||||
if (diag.slowest.length > 10) diag.slowest.length = 10;
|
||||
pushSlowest(diag.slowest, { teamName, ms }, 10);
|
||||
}
|
||||
return summary;
|
||||
});
|
||||
|
|
@ -290,59 +413,69 @@ async function listTeams(payload: ListTeamsPayload): Promise<{ teams: unknown[];
|
|||
return { teams, diag };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task normalization helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function normalizeWorkIntervals(
|
||||
parsed: any
|
||||
parsed: ParsedTask
|
||||
): { startedAt: string; completedAt?: string }[] | undefined {
|
||||
if (!Array.isArray(parsed?.workIntervals)) return undefined;
|
||||
if (!Array.isArray(parsed.workIntervals)) return undefined;
|
||||
return (parsed.workIntervals as unknown[])
|
||||
.filter(
|
||||
(i): i is { startedAt: string; completedAt?: string } =>
|
||||
(i): i is RawWorkInterval =>
|
||||
Boolean(i) &&
|
||||
typeof i === 'object' &&
|
||||
typeof (i as any).startedAt === 'string' &&
|
||||
((i as any).completedAt === undefined || typeof (i as any).completedAt === 'string')
|
||||
typeof (i as RawWorkInterval).startedAt === 'string' &&
|
||||
((i as RawWorkInterval).completedAt === undefined ||
|
||||
typeof (i as RawWorkInterval).completedAt === 'string')
|
||||
)
|
||||
.map((i) => ({ startedAt: i.startedAt, completedAt: i.completedAt }));
|
||||
.map((i) => ({
|
||||
startedAt: i.startedAt as string,
|
||||
completedAt: i.completedAt as string | undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeComments(parsed: any): unknown[] | undefined {
|
||||
if (!Array.isArray(parsed?.comments)) return undefined;
|
||||
function normalizeComments(parsed: ParsedTask): unknown[] | undefined {
|
||||
if (!Array.isArray(parsed.comments)) return undefined;
|
||||
return (parsed.comments as unknown[])
|
||||
.filter(
|
||||
(c) =>
|
||||
c &&
|
||||
(c): c is RawComment =>
|
||||
!!c &&
|
||||
typeof c === 'object' &&
|
||||
typeof (c as any).id === 'string' &&
|
||||
typeof (c as any).author === 'string' &&
|
||||
typeof (c as any).text === 'string' &&
|
||||
typeof (c as any).createdAt === 'string'
|
||||
typeof (c as RawComment).id === 'string' &&
|
||||
typeof (c as RawComment).author === 'string' &&
|
||||
typeof (c as RawComment).text === 'string' &&
|
||||
typeof (c as RawComment).createdAt === 'string'
|
||||
)
|
||||
.map((c) => ({
|
||||
id: (c as any).id,
|
||||
author: (c as any).author,
|
||||
text: (c as any).text,
|
||||
createdAt: (c as any).createdAt,
|
||||
id: c.id as string,
|
||||
author: c.author as string,
|
||||
text: c.text as string,
|
||||
createdAt: c.createdAt as string,
|
||||
type:
|
||||
(c as any).type === 'regular' ||
|
||||
(c as any).type === 'review_request' ||
|
||||
(c as any).type === 'review_approved'
|
||||
? (c as any).type
|
||||
c.type === 'regular' || c.type === 'review_request' || c.type === 'review_approved'
|
||||
? (c.type as string)
|
||||
: 'regular',
|
||||
}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getAllTasks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function readTasksDirForTeam(
|
||||
tasksDir: string,
|
||||
teamName: string,
|
||||
payload: GetAllTasksPayload,
|
||||
diag: any
|
||||
): Promise<unknown[]> {
|
||||
payload: GetAllTasksPayload
|
||||
): Promise<{ tasks: unknown[]; taskDiag: TaskReadDiag }> {
|
||||
const taskDiag: TaskReadDiag = { skipped: 0, skipReasons: {} };
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await fs.promises.readdir(tasksDir);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
return { tasks: [], taskDiag };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -362,23 +495,22 @@ async function readTasksDirForTeam(
|
|||
try {
|
||||
const stat = await fs.promises.stat(taskPath);
|
||||
if (!stat.isFile() || stat.size > payload.maxTaskBytes) {
|
||||
diag.skipped++;
|
||||
diag.skipReasons.task_not_file_or_large =
|
||||
(diag.skipReasons.task_not_file_or_large || 0) + 1;
|
||||
taskDiag.skipped++;
|
||||
bumpSkipReason(taskDiag.skipReasons, 'task_not_file_or_large');
|
||||
continue;
|
||||
}
|
||||
|
||||
const raw = await readFileUtf8WithTimeout(taskPath, payload.maxTaskReadMs);
|
||||
const parsed = JSON.parse(raw);
|
||||
const metadata = parsed?.metadata;
|
||||
const parsed = JSON.parse(raw) as ParsedTask;
|
||||
const metadata = parsed.metadata;
|
||||
if (metadata?._internal === true) {
|
||||
diag.skipped++;
|
||||
diag.skipReasons.task_internal = (diag.skipReasons.task_internal || 0) + 1;
|
||||
taskDiag.skipped++;
|
||||
bumpSkipReason(taskDiag.skipReasons, 'task_internal');
|
||||
continue;
|
||||
}
|
||||
if (parsed?.status === 'deleted') {
|
||||
diag.skipped++;
|
||||
diag.skipReasons.task_deleted = (diag.skipReasons.task_deleted || 0) + 1;
|
||||
if (parsed.status === 'deleted') {
|
||||
taskDiag.skipped++;
|
||||
bumpSkipReason(taskDiag.skipReasons, 'task_deleted');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -404,7 +536,7 @@ async function readTasksDirForTeam(
|
|||
|
||||
const needsClarification =
|
||||
parsed.needsClarification === 'lead' || parsed.needsClarification === 'user'
|
||||
? parsed.needsClarification
|
||||
? (parsed.needsClarification as string)
|
||||
: undefined;
|
||||
|
||||
tasks.push({
|
||||
|
|
@ -419,7 +551,7 @@ async function readTasksDirForTeam(
|
|||
parsed.status === 'in_progress' ||
|
||||
parsed.status === 'completed' ||
|
||||
parsed.status === 'deleted'
|
||||
? parsed.status
|
||||
? (parsed.status as string)
|
||||
: 'pending',
|
||||
workIntervals: normalizeWorkIntervals(parsed),
|
||||
blocks: Array.isArray(parsed.blocks) ? (parsed.blocks as unknown[]) : undefined,
|
||||
|
|
@ -436,23 +568,32 @@ async function readTasksDirForTeam(
|
|||
teamName,
|
||||
});
|
||||
} catch (error) {
|
||||
diag.skipped++;
|
||||
taskDiag.skipped++;
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code === 'READ_TIMEOUT') {
|
||||
diag.skipReasons.task_read_timeout = (diag.skipReasons.task_read_timeout || 0) + 1;
|
||||
bumpSkipReason(taskDiag.skipReasons, 'task_read_timeout');
|
||||
} else {
|
||||
diag.skipReasons.task_parse_failed = (diag.skipReasons.task_parse_failed || 0) + 1;
|
||||
bumpSkipReason(taskDiag.skipReasons, 'task_parse_failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
return tasks;
|
||||
return { tasks, taskDiag };
|
||||
}
|
||||
|
||||
function mergeTaskDiag(target: GetAllTasksDiag, source: TaskReadDiag): void {
|
||||
// eslint-disable-next-line no-param-reassign -- accumulator mutation is intentional
|
||||
target.skipped += source.skipped;
|
||||
for (const [reason, count] of Object.entries(source.skipReasons)) {
|
||||
// eslint-disable-next-line no-param-reassign -- accumulator mutation is intentional
|
||||
target.skipReasons[reason] = (target.skipReasons[reason] || 0) + count;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllTasks(
|
||||
payload: GetAllTasksPayload
|
||||
): Promise<{ tasks: unknown[]; diag: unknown }> {
|
||||
): Promise<{ tasks: unknown[]; diag: GetAllTasksDiag }> {
|
||||
const startedAt = nowMs();
|
||||
const diag: any = {
|
||||
const diag: GetAllTasksDiag = {
|
||||
op: 'getAllTasks',
|
||||
startedAt,
|
||||
tasksBase: payload.tasksBase,
|
||||
|
|
@ -483,17 +624,16 @@ async function getAllTasks(
|
|||
const t0 = nowMs();
|
||||
try {
|
||||
const tasksDir = path.join(payload.tasksBase, teamName);
|
||||
const tasks = await readTasksDirForTeam(tasksDir, teamName, payload, diag);
|
||||
const { tasks, taskDiag } = await readTasksDirForTeam(tasksDir, teamName, payload);
|
||||
mergeTaskDiag(diag, taskDiag);
|
||||
const ms = nowMs() - t0;
|
||||
if (ms >= 250) {
|
||||
diag.slowestTeams.push({ teamName, ms });
|
||||
diag.slowestTeams.sort((a: any, b: any) => b.ms - a.ms);
|
||||
if (diag.slowestTeams.length > 10) diag.slowestTeams.length = 10;
|
||||
pushSlowest(diag.slowestTeams, { teamName, ms }, 10);
|
||||
}
|
||||
return tasks;
|
||||
} catch {
|
||||
diag.skipped++;
|
||||
diag.skipReasons.team_dir_failed = (diag.skipReasons.team_dir_failed || 0) + 1;
|
||||
bumpSkipReason(diag.skipReasons, 'team_dir_failed');
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
|
@ -504,6 +644,10 @@ async function getAllTasks(
|
|||
return { tasks, diag };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Worker message handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function post(msg: WorkerResponse): void {
|
||||
parentPort?.postMessage(msg);
|
||||
}
|
||||
|
|
@ -521,7 +665,7 @@ parentPort?.on('message', async (msg: WorkerRequest) => {
|
|||
post({ id, ok: true, result: tasks, diag });
|
||||
return;
|
||||
}
|
||||
post({ id, ok: false, error: `Unknown op: ${String((msg as any).op)}` });
|
||||
post({ id, ok: false, error: `Unknown op: ${String(op)}` });
|
||||
} catch (error) {
|
||||
post({ id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ export function useDiffNavigation(
|
|||
useEffect(() => {
|
||||
if (!isDialogOpen) return;
|
||||
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
const handler = (event: KeyboardEvent): void => {
|
||||
if (event.defaultPrevented) return;
|
||||
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ export function useFileSuggestions(
|
|||
const cached = getQuickOpenCache(projectPath);
|
||||
if (cached) return;
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- setLoading before async fetch is intentional
|
||||
return fetchFiles(projectPath);
|
||||
}, [projectPath, fetchTrigger, fetchFiles]);
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ export function initializeNotificationListeners(): () => void {
|
|||
const nav: NavigatorWithUserAgentData | null =
|
||||
typeof navigator !== 'undefined' ? (navigator as NavigatorWithUserAgentData) : null;
|
||||
// Prefer UA-CH when available; fall back to deprecated-but-still-supported navigator.platform.
|
||||
const platform = nav?.userAgentData?.platform ?? nav?.platform ?? nav?.userAgent ?? '';
|
||||
// eslint-disable-next-line sonarjs/deprecation -- navigator.platform is deprecated but needed as fallback
|
||||
const platform: string = nav?.userAgentData?.platform ?? nav?.platform ?? nav?.userAgent ?? '';
|
||||
const isWindows = platform.toLowerCase().includes('win');
|
||||
const delayMs = isWindows ? 3000 : 0;
|
||||
cliStatusTimer = setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -335,7 +335,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
const rootParts = splitPath(root);
|
||||
const fileParts = splitPath(filePath);
|
||||
const win = isWindowsishPath(root);
|
||||
const eq = (a: string, b: string) => (win ? a.toLowerCase() === b.toLowerCase() : a === b);
|
||||
const eq = (a: string, b: string): boolean =>
|
||||
win ? a.toLowerCase() === b.toLowerCase() : a === b;
|
||||
const hasPrefix =
|
||||
fileParts.length >= rootParts.length && rootParts.every((seg, i) => eq(seg, fileParts[i]));
|
||||
|
||||
|
|
@ -1344,7 +1345,8 @@ function remapPath(p: string, oldPath: string, newPath: string): string {
|
|||
if (oldParts.length === 0) return p;
|
||||
|
||||
const win = isWindowsishPath(oldPath) || isWindowsishPath(p) || isWindowsishPath(newPath);
|
||||
const eq = (a: string, b: string) => (win ? a.toLowerCase() === b.toLowerCase() : a === b);
|
||||
const eq = (a: string, b: string): boolean =>
|
||||
win ? a.toLowerCase() === b.toLowerCase() : a === b;
|
||||
|
||||
const matchesPrefix =
|
||||
pParts.length >= oldParts.length && oldParts.every((seg, i) => eq(seg, pParts[i]));
|
||||
|
|
@ -1364,7 +1366,8 @@ function reverseRemapPath(p: string, oldPath: string, newPath: string): string {
|
|||
if (newParts.length === 0) return p;
|
||||
|
||||
const win = isWindowsishPath(oldPath) || isWindowsishPath(p) || isWindowsishPath(newPath);
|
||||
const eq = (a: string, b: string) => (win ? a.toLowerCase() === b.toLowerCase() : a === b);
|
||||
const eq = (a: string, b: string): boolean =>
|
||||
win ? a.toLowerCase() === b.toLowerCase() : a === b;
|
||||
|
||||
const matchesPrefix =
|
||||
pParts.length >= newParts.length && newParts.every((seg, i) => eq(seg, pParts[i]));
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ export function buildFileAction(
|
|||
const fullParts = splitPath(filePath);
|
||||
const rootParts = splitPath(projectPath);
|
||||
const win = isWindowsishPath(projectPath);
|
||||
const eq = (a: string, b: string) => (win ? a.toLowerCase() === b.toLowerCase() : a === b);
|
||||
const eq = (a: string, b: string): boolean =>
|
||||
win ? a.toLowerCase() === b.toLowerCase() : a === b;
|
||||
const hasPrefix =
|
||||
fullParts.length >= rootParts.length && rootParts.every((seg, i) => eq(seg, fullParts[i]));
|
||||
if (hasPrefix) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ export function stripTrailingSeparators(filePath: string): string {
|
|||
if (!filePath) return filePath;
|
||||
const p = filePath.replace(/\\/g, '/');
|
||||
if (p === '/' || /^[A-Za-z]:\/$/.test(p)) return filePath;
|
||||
return filePath.replace(/[/\\]+$/, '');
|
||||
// Trim trailing separators manually to avoid sonarjs/slow-regex
|
||||
let end = filePath.length;
|
||||
while (end > 0 && (filePath[end - 1] === '/' || filePath[end - 1] === '\\')) end--;
|
||||
return end === filePath.length ? filePath : filePath.slice(0, end);
|
||||
}
|
||||
|
||||
/** Prefer the separator style already present in the path. */
|
||||
|
|
@ -55,7 +58,12 @@ export function joinPath(base: string, ...segments: string[]): string {
|
|||
const sep = getPreferredSeparator(base);
|
||||
let out = stripTrailingSeparators(base);
|
||||
for (const seg of segments) {
|
||||
const cleaned = seg.replace(/^[\\/]+|[\\/]+$/g, '');
|
||||
// Trim leading and trailing separators manually to avoid sonarjs/slow-regex
|
||||
let start = 0;
|
||||
while (start < seg.length && (seg[start] === '/' || seg[start] === '\\')) start++;
|
||||
let segEnd = seg.length;
|
||||
while (segEnd > start && (seg[segEnd - 1] === '/' || seg[segEnd - 1] === '\\')) segEnd--;
|
||||
const cleaned = seg.slice(start, segEnd);
|
||||
if (!cleaned) continue;
|
||||
if (!out || out.endsWith('/') || out.endsWith('\\')) {
|
||||
out += cleaned;
|
||||
|
|
|
|||
Loading…
Reference in a new issue