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 logger = createLogger('Service:MemberStatsComputer');
const TRAILING_PUNCT = /[;.,]+$/; const TRAILING_PUNCT_CHARS = new Set([';', '.', ',']);
const INVALID_NAMES = new Set(['null', 'undefined', 'None', 'false', 'true', '']); 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 { 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('/'); 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 // Track last known content per file for accurate Write/NotebookEdit diffs
const fileLastContent = new Map<string, string>(); 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 => { const trackFile = (fp: string): void => {
if (typeof fp === 'string') { if (typeof fp === 'string') {

View file

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

View file

@ -21,8 +21,8 @@ import { getMemberColor } from '@shared/constants/memberColors';
import { resolveLanguageName } from '@shared/utils/agentLanguage'; import { resolveLanguageName } from '@shared/utils/agentLanguage';
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise'; import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName'; import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName';
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import * as fs from 'fs'; import * as fs from 'fs';
@ -2971,8 +2971,8 @@ export class TeamProvisioningService {
) { ) {
const input = (block.input ?? {}) as Record<string, unknown>; const input = (block.input ?? {}) as Record<string, unknown>;
run.pendingToolCalls.push({ run.pendingToolCalls.push({
name: block.name as string, name: block.name,
preview: extractToolPreview(block.name as string, input), 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. */ /** Must match the `w-80` (320px) context panel width used in the layout below. */
const CONTEXT_PANEL_WIDTH_PX = 320; const CONTEXT_PANEL_WIDTH_PX = 320;
import { formatPercentOfTotal, sumContextInjectionTokens } from '@renderer/utils/contextMath';
import { ChatHistoryEmptyState } from './ChatHistoryEmptyState'; import { ChatHistoryEmptyState } from './ChatHistoryEmptyState';
import { ChatHistoryItem } from './ChatHistoryItem'; import { ChatHistoryItem } from './ChatHistoryItem';
import { ChatHistoryLoadingState } from './ChatHistoryLoadingState'; import { ChatHistoryLoadingState } from './ChatHistoryLoadingState';
import { formatPercentOfTotal, sumContextInjectionTokens } from '@renderer/utils/contextMath';
import type { ContextInjection } from '@renderer/types/contextInjection'; 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 { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { getTeamColorSet } from '@renderer/constants/teamColors'; import { getTeamColorSet } from '@renderer/constants/teamColors';
import { nameColorSet } from '@renderer/utils/projectColor';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { nameColorSet } from '@renderer/utils/projectColor';
import { import {
Activity, Activity,
Bell, Bell,

View file

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

View file

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { RoleSelect } from '@renderer/components/team/RoleSelect';
import { TeamModelSelector } from '@renderer/components/team/dialogs/TeamModelSelector'; import { TeamModelSelector } from '@renderer/components/team/dialogs/TeamModelSelector';
import { RoleSelect } from '@renderer/components/team/RoleSelect';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { Input } from '@renderer/components/ui/input'; import { Input } from '@renderer/components/ui/input';
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; 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 [tipIndex, setTipIndex] = React.useState(0);
const [tipVisible, setTipVisible] = React.useState(true); const [tipVisible, setTipVisible] = React.useState(true);
const advanceTip = React.useCallback(() => {
setTipIndex((prev) => (prev + 1) % rotatingTips.length);
setTipVisible(true);
}, [rotatingTips.length]);
React.useEffect(() => { React.useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
setTipVisible(false); setTipVisible(false);
setTimeout(() => { setTimeout(advanceTip, 300);
setTipIndex((prev) => (prev + 1) % rotatingTips.length);
setTipVisible(true);
}, 300);
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [rotatingTips.length]); }, [advanceTip]);
const resolvedHintText = hintText ?? rotatingTips[tipIndex]; const resolvedHintText = hintText ?? rotatingTips[tipIndex];
const showHintRow = showHint && (suggestions.length > 0 || enableFiles); 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 prevByName = get().teamByName;
const existingEntry = prevByName[teamName]; const existingEntry = prevByName[teamName];
const configColor = data.config.color; const configColor = data.config.color;
if (configColor && (!existingEntry || existingEntry.color !== configColor)) { if (configColor && (!existingEntry || existingEntry?.color !== configColor)) {
const patched: TeamSummary = existingEntry const patched: TeamSummary = existingEntry
? { ...existingEntry, color: configColor, displayName: data.config.name || teamName } ? { ...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 { export function parseToolSummary(summary: string | undefined): ToolSummaryData | null {
if (!summary) return 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; if (!match) return null;
const byName: Record<string, number> = {}; const byName: Record<string, number> = {};
for (const part of match[2].split(', ')) { for (const part of match[2].split(', ')) {
const m = part.match(/^(\d+)\s+(.+)$/); const m = /^(\d+)\s+(\S+(?:\s+\S+)*)$/.exec(part);
if (m) { if (m) {
byName[m[2]] = parseInt(m[1], 10); byName[m[2]] = parseInt(m[1], 10);
} else { } else {
@ -96,9 +96,9 @@ export function extractToolPreview(
case 'Agent': case 'Agent':
case 'TaskCreate': case 'TaskCreate':
return typeof input.prompt === 'string' return typeof input.prompt === 'string'
? truncateStr(input.prompt, 200) ? input.prompt
: typeof input.description === 'string' : typeof input.description === 'string'
? truncateStr(input.description, 200) ? input.description
: undefined; : undefined;
case 'WebFetch': case 'WebFetch':
if (typeof input.url === 'string') { if (typeof input.url === 'string') {