fix: resolve review bugs and lint errors

- teams.ts: add type validation in handleCreateConfig for displayName/description/color
- CreateTaskDialog: scope draft keys per team to prevent cross-team leakage
- agentBlocks.ts: replace stateful singleton regex with factory function
- teams.test.ts: add missing channel assertions and use os.tmpdir()
- SendMessageDialog: move setState out of useEffect to render phase
- TeamMemberLogsFinder: remove unused projectId destructuring
- MentionableTextarea: add eslint-disable description
- useMentionDetection: replace deprecated wordWrap with overflowWrap
This commit is contained in:
iliya 2026-02-22 18:58:35 +02:00 committed by Илия
parent 704b9cbfe5
commit 42e4b0f4aa
10 changed files with 48 additions and 15 deletions

View file

@ -690,6 +690,16 @@ async function handleCreateConfig(
return { success: false, error: 'members must contain at least one member' };
}
if (payload.displayName !== undefined && typeof payload.displayName !== 'string') {
return { success: false, error: 'displayName must be a string' };
}
if (payload.description !== undefined && typeof payload.description !== 'string') {
return { success: false, error: 'description must be a string' };
}
if (payload.color !== undefined && typeof payload.color !== 'string') {
return { success: false, error: 'color must be a string' };
}
const seenNames = new Set<string>();
const members: TeamCreateConfigRequest['members'] = [];
for (const member of payload.members) {

View file

@ -110,7 +110,7 @@ export class TeamMemberLogsFinder {
const discovery = await this.discoverMemberFiles(teamName, memberName);
if (!discovery) return [];
const { projectDir, projectId, config, sessionIds, knownMembers, isLeadMember } = discovery;
const { projectDir, config, sessionIds, knownMembers, isLeadMember } = discovery;
const paths: string[] = [];
if (isLeadMember && config.leadSessionId) {

View file

@ -652,6 +652,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
<CreateTaskDialog
open={createTaskDialog.open}
teamName={teamName}
members={data.members}
tasks={data.tasks}
defaultSubject={createTaskDialog.defaultSubject}

View file

@ -14,7 +14,7 @@ import {
parseStructuredAgentMessage,
} from '@renderer/utils/agentMessageFormatting';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { AGENT_BLOCK_REGEX } from '@shared/constants/agentBlocks';
import { createAgentBlockRegex } from '@shared/constants/agentBlocks';
import { Bot, ChevronRight, ListPlus, MessageSquare } from 'lucide-react';
import type { TeamColorSet } from '@renderer/constants/teamColors';
@ -109,7 +109,7 @@ function getSystemMessageLabel(text: string): string | null {
/** Strip ```info_for_agent ... ``` blocks from text for UI display. */
function stripAgentBlocks(text: string): string {
return text.replace(AGENT_BLOCK_REGEX, '').trim();
return text.replace(createAgentBlockRegex(), '').trim();
}
// ---------------------------------------------------------------------------

View file

@ -30,6 +30,7 @@ import type { ResolvedTeamMember, TeamTask } from '@shared/types';
interface CreateTaskDialogProps {
open: boolean;
teamName: string;
members: ResolvedTeamMember[];
tasks: TeamTask[];
defaultSubject?: string;
@ -49,6 +50,7 @@ interface CreateTaskDialogProps {
export const CreateTaskDialog = ({
open,
teamName,
members,
tasks,
defaultSubject = '',
@ -60,13 +62,13 @@ export const CreateTaskDialog = ({
}: CreateTaskDialogProps): React.JSX.Element => {
const [subject, setSubject] = useState(defaultSubject);
const descriptionDraft = useDraftPersistence({
key: 'createTask:description',
key: `createTask:${teamName}:description`,
initialValue: defaultDescription || undefined,
});
const [owner, setOwner] = useState<string>(defaultOwner);
const [blockedBy, setBlockedBy] = useState<string[]>([]);
const [startImmediately, setStartImmediately] = useState(true);
const promptDraft = useDraftPersistence({ key: 'createTask:prompt' });
const promptDraft = useDraftPersistence({ key: `createTask:${teamName}:prompt` });
const [prevOpen, setPrevOpen] = useState(false);
if (open && !prevOpen) {

View file

@ -69,19 +69,20 @@ export const SendMessageDialog = ({
const [pendingAutoClose, setPendingAutoClose] = useState(false);
if (open && lastResult && lastResult !== prevResult) {
setPrevResult(lastResult);
setMember('');
setSummary('');
setPendingAutoClose(true);
}
// Side effects (onClose mutates parent state) must run in useEffect, not render phase
useEffect(() => {
if (pendingAutoClose) {
setMember('');
textDraft.clearDraft();
setSummary('');
setPendingAutoClose(false);
onClose();
}
}, [pendingAutoClose]); // eslint-disable-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps -- only trigger on pendingAutoClose flag
}, [pendingAutoClose]);
const mentionSuggestions = useMemo<MentionSuggestion[]>(
() =>

View file

@ -69,7 +69,7 @@ function parseMentionSegments(text: string, suggestions: MentionSuggestion[]): S
// Character after name must be boundary
if (end < text.length) {
const after = text[end];
// eslint-disable-next-line no-useless-escape
// eslint-disable-next-line no-useless-escape -- escaped chars needed for regex character class
if (!/[\s,.:;!?\)\]\}\-]/.test(after)) continue;
}

View file

@ -82,7 +82,7 @@ export function getCaretCoordinates(
mirror.style.position = 'absolute';
mirror.style.visibility = 'hidden';
mirror.style.whiteSpace = 'pre-wrap';
mirror.style.wordWrap = 'break-word';
mirror.style.overflowWrap = 'break-word';
mirror.style.overflow = 'hidden';
for (const prop of MIRROR_PROPS) {

View file

@ -13,10 +13,21 @@ export const AGENT_BLOCK_OPEN = '```' + AGENT_BLOCK_TAG;
export const AGENT_BLOCK_CLOSE = '```';
/**
* Regex that matches a full ``` info_for_agent ... ``` block (including fences).
* Regex pattern string for matching ``` info_for_agent ... ``` blocks (including fences).
* Supports optional leading/trailing whitespace and newlines around the block.
*/
export const AGENT_BLOCK_REGEX = new RegExp(
'\\n?```' + AGENT_BLOCK_TAG + '\\n[\\s\\S]*?\\n```\\n?',
'g'
);
const AGENT_BLOCK_PATTERN = '\\n?```' + AGENT_BLOCK_TAG + '\\n[\\s\\S]*?\\n```\\n?';
/**
* Creates a new RegExp for matching agent blocks.
* Returns a fresh instance each time to avoid stateful 'g' flag issues with .test().
*/
export function createAgentBlockRegex(): RegExp {
return new RegExp(AGENT_BLOCK_PATTERN, 'g');
}
/**
* @deprecated Use createAgentBlockRegex() instead to avoid stateful 'g' flag issues.
* Kept for backward compatibility with .replace() calls.
*/
export const AGENT_BLOCK_REGEX = new RegExp(AGENT_BLOCK_PATTERN, 'g');

View file

@ -43,7 +43,9 @@ import {
TEAM_PROVISIONING_STATUS,
TEAM_REQUEST_REVIEW,
TEAM_SEND_MESSAGE,
TEAM_GET_ALL_TASKS,
TEAM_GET_MEMBER_LOGS,
TEAM_GET_MEMBER_STATS,
TEAM_START_TASK,
TEAM_UPDATE_CONFIG,
TEAM_UPDATE_KANBAN,
@ -125,7 +127,9 @@ describe('ipc teams handlers', () => {
expect(handlers.has(TEAM_ALIVE_LIST)).toBe(true);
expect(handlers.has(TEAM_CREATE_CONFIG)).toBe(true);
expect(handlers.has(TEAM_GET_MEMBER_LOGS)).toBe(true);
expect(handlers.has(TEAM_GET_MEMBER_STATS)).toBe(true);
expect(handlers.has(TEAM_UPDATE_CONFIG)).toBe(true);
expect(handlers.has(TEAM_GET_ALL_TASKS)).toBe(true);
});
it('returns success false on invalid sendMessage args', async () => {
@ -289,5 +293,9 @@ describe('ipc teams handlers', () => {
expect(handlers.has(TEAM_PROCESS_ALIVE)).toBe(false);
expect(handlers.has(TEAM_ALIVE_LIST)).toBe(false);
expect(handlers.has(TEAM_CREATE_CONFIG)).toBe(false);
expect(handlers.has(TEAM_GET_MEMBER_LOGS)).toBe(false);
expect(handlers.has(TEAM_GET_MEMBER_STATS)).toBe(false);
expect(handlers.has(TEAM_UPDATE_CONFIG)).toBe(false);
expect(handlers.has(TEAM_GET_ALL_TASKS)).toBe(false);
});
});