diff --git a/src/main/services/team/MemberStatsComputer.ts b/src/main/services/team/MemberStatsComputer.ts index 738cd80e..96ce9994 100644 --- a/src/main/services/team/MemberStatsComputer.ts +++ b/src/main/services/team/MemberStatsComputer.ts @@ -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(); - 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') { diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index 7fa3fda3..5b4d3ede 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -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 { // 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; toolCallsList.push({ - name: b.name as string, - preview: extractToolPreview(b.name as string, input), + name: b.name, + preview: extractToolPreview(b.name, input), }); } } diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 81038ae3..e80a3342 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -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; run.pendingToolCalls.push({ - name: block.name as string, - preview: extractToolPreview(block.name as string, input), + name: block.name, + preview: extractToolPreview(block.name, input), }); } } diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx index 0b5ad6ab..20a6f69a 100644 --- a/src/renderer/components/chat/ChatHistory.tsx +++ b/src/renderer/components/chat/ChatHistory.tsx @@ -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'; /** diff --git a/src/renderer/components/layout/SortableTab.tsx b/src/renderer/components/layout/SortableTab.tsx index b6cb0345..499849f4 100644 --- a/src/renderer/components/layout/SortableTab.tsx +++ b/src/renderer/components/layout/SortableTab.tsx @@ -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, diff --git a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx index cc42e2e9..67f41d31 100644 --- a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +++ b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx @@ -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 (
@@ -118,14 +118,14 @@ function ToolSummaryTooltipContent({ {toolCalls.map((tc, i) => { const isAgent = tc.name === 'Agent' || tc.name === 'TaskCreate'; return ( -
+
{isAgent ? '🤖 ' : ''} {tc.name} {tc.preview && ( {tc.preview} @@ -158,7 +158,7 @@ function ToolSummaryTooltipContent({ } return {toolSummary ?? ''}; -} +}; 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} - + (
{idx > 0 && ( -
+

- + { + 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); diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index c00da33a..204f0fe2 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -607,7 +607,7 @@ export const createTeamSlice: StateCreator = (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 } : { diff --git a/src/shared/utils/toolSummary.ts b/src/shared/utils/toolSummary.ts index 8f8c3ce2..72ee83be 100644 --- a/src/shared/utils/toolSummary.ts +++ b/src/shared/utils/toolSummary.ts @@ -27,11 +27,11 @@ export function buildToolSummary(content: Record[]): 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 = {}; 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') {