fix: resolve all ESLint errors breaking CI validate job

- Fix import sort order in 6 files (TeamDataService, TeamProvisioningService,
  ChatHistory, SortableTab, LeadThoughtsGroup, MemberDraftRow)
- Replace slow regex with string manipulation in MemberStatsComputer
- Use RegExp.exec() instead of String.match() in toolSummary
- Fix non-backtracking regex pattern in toolSummary parser
- Remove unnecessary type assertions in TeamDataService and TeamProvisioningService
- Convert for-loop to for-of in TeamDataService
- Fix no-param-reassign in TeamDataService.sendMessage
- Convert function component to arrow function in LeadThoughtsGroup
- Fix nested functions depth in MentionableTextarea tip rotation
- Use optional chain in teamSlice
This commit is contained in:
iliya 2026-03-06 16:36:37 +02:00
parent f61077d4ee
commit baabae0594
10 changed files with 54 additions and 40 deletions

View file

@ -9,11 +9,17 @@ import type { FileLineStats, MemberFullStats } from '@shared/types';
const logger = createLogger('Service:MemberStatsComputer');
const TRAILING_PUNCT = /[;.,]+$/;
const TRAILING_PUNCT_CHARS = new Set([';', '.', ',']);
const INVALID_NAMES = new Set(['null', 'undefined', 'None', 'false', 'true', '']);
function stripTrailingPunct(s: string): string {
let end = s.length;
while (end > 0 && TRAILING_PUNCT_CHARS.has(s[end - 1])) end--;
return end === s.length ? s : s.slice(0, end);
}
export function isValidFilePath(value: string): boolean {
const cleaned = value.trim().replace(TRAILING_PUNCT, '');
const cleaned = stripTrailingPunct(value.trim());
return cleaned.length > 1 && !INVALID_NAMES.has(cleaned) && cleaned.includes('/');
}
@ -133,7 +139,7 @@ export class MemberStatsComputer {
// Track last known content per file for accurate Write/NotebookEdit diffs
const fileLastContent = new Map<string, string>();
const cleanPath = (fp: string): string => fp.trim().replace(TRAILING_PUNCT, '');
const cleanPath = (fp: string): string => stripTrailingPunct(fp.trim());
const trackFile = (fp: string): void => {
if (typeof fp === 'string') {

View file

@ -59,8 +59,8 @@ import type {
TeamTask,
TeamTaskStatus,
TeamTaskWithKanban,
UpdateKanbanPatch,
ToolCallMeta,
UpdateKanbanPatch,
} from '@shared/types';
const logger = createLogger('Service:TeamDataService');
@ -344,12 +344,12 @@ export class TeamDataService {
// Find closest anchor by timestamp (binary-search-like scan from current position)
let bestAnchor = anchors[0];
let bestDist = Math.abs(msgTime - bestAnchor.time);
for (let a = 0; a < anchors.length; a++) {
const dist = Math.abs(msgTime - anchors[a].time);
for (const anchor of anchors) {
const dist = Math.abs(msgTime - anchor.time);
if (dist < bestDist) {
bestDist = dist;
bestAnchor = anchors[a];
} else if (dist > bestDist && anchors[a].time > msgTime) {
bestAnchor = anchor;
} else if (dist > bestDist && anchor.time > msgTime) {
// Anchors are sorted by index (asc time) — once distance grows past the
// message time, further anchors will only be farther.
break;
@ -1161,17 +1161,18 @@ export class TeamDataService {
async sendMessage(teamName: string, request: SendMessageRequest): Promise<SendMessageResult> {
// Enrich with leadSessionId so session boundary separators work
if (!request.leadSessionId) {
let enrichedRequest = request;
if (!enrichedRequest.leadSessionId) {
try {
const config = await this.configReader.getConfig(teamName);
if (config?.leadSessionId) {
request = { ...request, leadSessionId: config.leadSessionId };
enrichedRequest = { ...enrichedRequest, leadSessionId: config.leadSessionId };
}
} catch {
// non-critical
}
}
return this.inboxWriter.sendMessage(teamName, request);
return this.inboxWriter.sendMessage(teamName, enrichedRequest);
}
private resolveLeadNameFromConfig(config: TeamConfig | null): string {
@ -1528,8 +1529,8 @@ export class TeamDataService {
if (b.type === 'tool_use' && typeof b.name === 'string' && b.name !== 'SendMessage') {
const input = (b.input ?? {}) as Record<string, unknown>;
toolCallsList.push({
name: b.name as string,
preview: extractToolPreview(b.name as string, input),
name: b.name,
preview: extractToolPreview(b.name, input),
});
}
}

View file

@ -21,8 +21,8 @@ import { getMemberColor } from '@shared/constants/memberColors';
import { resolveLanguageName } from '@shared/utils/agentLanguage';
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { createLogger } from '@shared/utils/logger';
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName';
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
import { spawn } from 'child_process';
import { randomUUID } from 'crypto';
import * as fs from 'fs';
@ -2971,8 +2971,8 @@ export class TeamProvisioningService {
) {
const input = (block.input ?? {}) as Record<string, unknown>;
run.pendingToolCalls.push({
name: block.name as string,
preview: extractToolPreview(block.name as string, input),
name: block.name,
preview: extractToolPreview(block.name, input),
});
}
}

View file

@ -16,11 +16,12 @@ const SCROLL_THRESHOLD = 300;
/** Must match the `w-80` (320px) context panel width used in the layout below. */
const CONTEXT_PANEL_WIDTH_PX = 320;
import { formatPercentOfTotal, sumContextInjectionTokens } from '@renderer/utils/contextMath';
import { ChatHistoryEmptyState } from './ChatHistoryEmptyState';
import { ChatHistoryItem } from './ChatHistoryItem';
import { ChatHistoryLoadingState } from './ChatHistoryLoadingState';
import { formatPercentOfTotal, sumContextInjectionTokens } from '@renderer/utils/contextMath';
import type { ContextInjection } from '@renderer/types/contextInjection';
/**

View file

@ -8,8 +8,8 @@ import { useCallback, useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { nameColorSet } from '@renderer/utils/projectColor';
import { useStore } from '@renderer/store';
import { nameColorSet } from '@renderer/utils/projectColor';
import {
Activity,
Bell,

View file

@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import {
CARD_BG,
CARD_BG_ZEBRA,
@ -10,7 +11,6 @@ import {
CARD_TEXT_LIGHT,
} from '@renderer/constants/cssVariables';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store';
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
@ -102,13 +102,13 @@ function isRecentTimestamp(timestamp: string): boolean {
return Date.now() - t <= LIVE_WINDOW_MS;
}
function ToolSummaryTooltipContent({
const ToolSummaryTooltipContent = ({
toolCalls,
toolSummary,
}: {
}: Readonly<{
toolCalls?: ToolCallMeta[];
toolSummary?: string;
}): JSX.Element {
}>): JSX.Element => {
if (toolCalls && toolCalls.length > 0) {
return (
<div className="flex max-h-[300px] flex-col gap-0.5 overflow-y-auto">
@ -118,14 +118,14 @@ function ToolSummaryTooltipContent({
{toolCalls.map((tc, i) => {
const isAgent = tc.name === 'Agent' || tc.name === 'TaskCreate';
return (
<div key={i} className={`flex items-baseline gap-2 ${isAgent ? 'mt-0.5' : ''}`}>
<div key={i} className={isAgent ? 'mt-0.5' : 'flex items-baseline gap-2'}>
<span className={`shrink-0 font-semibold ${isAgent ? 'text-violet-400' : ''}`}>
{isAgent ? '🤖 ' : ''}
{tc.name}
</span>
{tc.preview && (
<span
className={`text-text-secondary ${isAgent ? 'whitespace-pre-wrap' : 'truncate'}`}
className={`text-text-secondary ${isAgent ? 'mt-0.5 block text-[10px]' : 'truncate'}`}
>
{tc.preview}
</span>
@ -158,7 +158,7 @@ function ToolSummaryTooltipContent({
}
return <span>{toolSummary ?? ''}</span>;
}
};
export const LeadThoughtsGroupRow = ({
group,
@ -268,7 +268,7 @@ export const LeadThoughtsGroupRow = ({
});
observer.observe(el);
return () => observer.disconnect();
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- scrollRef is stable
}, []);
const handleScroll = useCallback(() => {
const el = scrollRef.current;
@ -318,7 +318,7 @@ export const LeadThoughtsGroupRow = ({
{totalToolSummary}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="font-mono text-[11px]">
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
<ToolSummaryTooltipContent
toolCalls={allToolCalls}
toolSummary={totalToolSummary}
@ -344,7 +344,7 @@ export const LeadThoughtsGroupRow = ({
{chronologicalThoughts.map((thought, idx) => (
<div key={thought.messageId ?? idx} className="thought-expand-in">
{idx > 0 && (
<div className="mx-auto flex w-[40%] items-center justify-center gap-[5px] py-px">
<div className="mx-auto flex w-2/5 items-center justify-center gap-[5px] py-px">
<hr
className="flex-1 border-0"
style={{
@ -386,7 +386,11 @@ export const LeadThoughtsGroupRow = ({
🔧 {thought.toolSummary}
</div>
</TooltipTrigger>
<TooltipContent side="top" align="start" className="font-mono text-[11px]">
<TooltipContent
side="top"
align="start"
className="max-w-[420px] font-mono text-[11px]"
>
<ToolSummaryTooltipContent
toolCalls={thought.toolCalls}
toolSummary={thought.toolSummary}

View file

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { RoleSelect } from '@renderer/components/team/RoleSelect';
import { TeamModelSelector } from '@renderer/components/team/dialogs/TeamModelSelector';
import { RoleSelect } from '@renderer/components/team/RoleSelect';
import { Button } from '@renderer/components/ui/button';
import { Input } from '@renderer/components/ui/input';
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';

View file

@ -607,16 +607,18 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
const [tipIndex, setTipIndex] = React.useState(0);
const [tipVisible, setTipVisible] = React.useState(true);
const advanceTip = React.useCallback(() => {
setTipIndex((prev) => (prev + 1) % rotatingTips.length);
setTipVisible(true);
}, [rotatingTips.length]);
React.useEffect(() => {
const interval = setInterval(() => {
setTipVisible(false);
setTimeout(() => {
setTipIndex((prev) => (prev + 1) % rotatingTips.length);
setTipVisible(true);
}, 300);
setTimeout(advanceTip, 300);
}, 10000);
return () => clearInterval(interval);
}, [rotatingTips.length]);
}, [advanceTip]);
const resolvedHintText = hintText ?? rotatingTips[tipIndex];
const showHintRow = showHint && (suggestions.length > 0 || enableFiles);

View file

@ -607,7 +607,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
const prevByName = get().teamByName;
const existingEntry = prevByName[teamName];
const configColor = data.config.color;
if (configColor && (!existingEntry || existingEntry.color !== configColor)) {
if (configColor && (!existingEntry || existingEntry?.color !== configColor)) {
const patched: TeamSummary = existingEntry
? { ...existingEntry, color: configColor, displayName: data.config.name || teamName }
: {

View file

@ -27,11 +27,11 @@ export function buildToolSummary(content: Record<string, unknown>[]): string | u
export function parseToolSummary(summary: string | undefined): ToolSummaryData | null {
if (!summary) return null;
const match = summary.match(/^(\d+)\s+tools?\s+\(([^)]+)\)$/);
const match = /^(\d+)\s+tools?\s+\(([^)]+)\)$/.exec(summary);
if (!match) return null;
const byName: Record<string, number> = {};
for (const part of match[2].split(', ')) {
const m = part.match(/^(\d+)\s+(.+)$/);
const m = /^(\d+)\s+(\S+(?:\s+\S+)*)$/.exec(part);
if (m) {
byName[m[2]] = parseInt(m[1], 10);
} else {
@ -96,9 +96,9 @@ export function extractToolPreview(
case 'Agent':
case 'TaskCreate':
return typeof input.prompt === 'string'
? truncateStr(input.prompt, 200)
? input.prompt
: typeof input.description === 'string'
? truncateStr(input.description, 200)
? input.description
: undefined;
case 'WebFetch':
if (typeof input.url === 'string') {