diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index 574a46c0..ff362906 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -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(); const members: TeamCreateConfigRequest['members'] = []; for (const member of payload.members) { diff --git a/src/main/services/team/TeamMemberLogsFinder.ts b/src/main/services/team/TeamMemberLogsFinder.ts index a6a9bd11..06040dcc 100644 --- a/src/main/services/team/TeamMemberLogsFinder.ts +++ b/src/main/services/team/TeamMemberLogsFinder.ts @@ -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) { diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 27c98d41..e8e70759 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -652,6 +652,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele { const [subject, setSubject] = useState(defaultSubject); const descriptionDraft = useDraftPersistence({ - key: 'createTask:description', + key: `createTask:${teamName}:description`, initialValue: defaultDescription || undefined, }); const [owner, setOwner] = useState(defaultOwner); const [blockedBy, setBlockedBy] = useState([]); 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) { diff --git a/src/renderer/components/team/dialogs/SendMessageDialog.tsx b/src/renderer/components/team/dialogs/SendMessageDialog.tsx index 034ff50b..439ebbb2 100644 --- a/src/renderer/components/team/dialogs/SendMessageDialog.tsx +++ b/src/renderer/components/team/dialogs/SendMessageDialog.tsx @@ -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( () => diff --git a/src/renderer/components/ui/MentionableTextarea.tsx b/src/renderer/components/ui/MentionableTextarea.tsx index efd0791c..00b55d74 100644 --- a/src/renderer/components/ui/MentionableTextarea.tsx +++ b/src/renderer/components/ui/MentionableTextarea.tsx @@ -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; } diff --git a/src/renderer/hooks/useMentionDetection.ts b/src/renderer/hooks/useMentionDetection.ts index 6731bc3c..c0ddd3be 100644 --- a/src/renderer/hooks/useMentionDetection.ts +++ b/src/renderer/hooks/useMentionDetection.ts @@ -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) { diff --git a/src/shared/constants/agentBlocks.ts b/src/shared/constants/agentBlocks.ts index 8301b151..30aa2800 100644 --- a/src/shared/constants/agentBlocks.ts +++ b/src/shared/constants/agentBlocks.ts @@ -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'); diff --git a/test/main/ipc/teams.test.ts b/test/main/ipc/teams.test.ts index 5a263fd7..0222d323 100644 --- a/test/main/ipc/teams.test.ts +++ b/test/main/ipc/teams.test.ts @@ -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); }); });