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:
parent
704b9cbfe5
commit
42e4b0f4aa
10 changed files with 48 additions and 15 deletions
|
|
@ -690,6 +690,16 @@ async function handleCreateConfig(
|
||||||
return { success: false, error: 'members must contain at least one member' };
|
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 seenNames = new Set<string>();
|
||||||
const members: TeamCreateConfigRequest['members'] = [];
|
const members: TeamCreateConfigRequest['members'] = [];
|
||||||
for (const member of payload.members) {
|
for (const member of payload.members) {
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ export class TeamMemberLogsFinder {
|
||||||
const discovery = await this.discoverMemberFiles(teamName, memberName);
|
const discovery = await this.discoverMemberFiles(teamName, memberName);
|
||||||
if (!discovery) return [];
|
if (!discovery) return [];
|
||||||
|
|
||||||
const { projectDir, projectId, config, sessionIds, knownMembers, isLeadMember } = discovery;
|
const { projectDir, config, sessionIds, knownMembers, isLeadMember } = discovery;
|
||||||
const paths: string[] = [];
|
const paths: string[] = [];
|
||||||
|
|
||||||
if (isLeadMember && config.leadSessionId) {
|
if (isLeadMember && config.leadSessionId) {
|
||||||
|
|
|
||||||
|
|
@ -652,6 +652,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
|
|
||||||
<CreateTaskDialog
|
<CreateTaskDialog
|
||||||
open={createTaskDialog.open}
|
open={createTaskDialog.open}
|
||||||
|
teamName={teamName}
|
||||||
members={data.members}
|
members={data.members}
|
||||||
tasks={data.tasks}
|
tasks={data.tasks}
|
||||||
defaultSubject={createTaskDialog.defaultSubject}
|
defaultSubject={createTaskDialog.defaultSubject}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
parseStructuredAgentMessage,
|
parseStructuredAgentMessage,
|
||||||
} from '@renderer/utils/agentMessageFormatting';
|
} from '@renderer/utils/agentMessageFormatting';
|
||||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
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 { Bot, ChevronRight, ListPlus, MessageSquare } from 'lucide-react';
|
||||||
|
|
||||||
import type { TeamColorSet } from '@renderer/constants/teamColors';
|
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. */
|
/** Strip ```info_for_agent ... ``` blocks from text for UI display. */
|
||||||
function stripAgentBlocks(text: string): string {
|
function stripAgentBlocks(text: string): string {
|
||||||
return text.replace(AGENT_BLOCK_REGEX, '').trim();
|
return text.replace(createAgentBlockRegex(), '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import type { ResolvedTeamMember, TeamTask } from '@shared/types';
|
||||||
|
|
||||||
interface CreateTaskDialogProps {
|
interface CreateTaskDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
teamName: string;
|
||||||
members: ResolvedTeamMember[];
|
members: ResolvedTeamMember[];
|
||||||
tasks: TeamTask[];
|
tasks: TeamTask[];
|
||||||
defaultSubject?: string;
|
defaultSubject?: string;
|
||||||
|
|
@ -49,6 +50,7 @@ interface CreateTaskDialogProps {
|
||||||
|
|
||||||
export const CreateTaskDialog = ({
|
export const CreateTaskDialog = ({
|
||||||
open,
|
open,
|
||||||
|
teamName,
|
||||||
members,
|
members,
|
||||||
tasks,
|
tasks,
|
||||||
defaultSubject = '',
|
defaultSubject = '',
|
||||||
|
|
@ -60,13 +62,13 @@ export const CreateTaskDialog = ({
|
||||||
}: CreateTaskDialogProps): React.JSX.Element => {
|
}: CreateTaskDialogProps): React.JSX.Element => {
|
||||||
const [subject, setSubject] = useState(defaultSubject);
|
const [subject, setSubject] = useState(defaultSubject);
|
||||||
const descriptionDraft = useDraftPersistence({
|
const descriptionDraft = useDraftPersistence({
|
||||||
key: 'createTask:description',
|
key: `createTask:${teamName}:description`,
|
||||||
initialValue: defaultDescription || undefined,
|
initialValue: defaultDescription || undefined,
|
||||||
});
|
});
|
||||||
const [owner, setOwner] = useState<string>(defaultOwner);
|
const [owner, setOwner] = useState<string>(defaultOwner);
|
||||||
const [blockedBy, setBlockedBy] = useState<string[]>([]);
|
const [blockedBy, setBlockedBy] = useState<string[]>([]);
|
||||||
const [startImmediately, setStartImmediately] = useState(true);
|
const [startImmediately, setStartImmediately] = useState(true);
|
||||||
const promptDraft = useDraftPersistence({ key: 'createTask:prompt' });
|
const promptDraft = useDraftPersistence({ key: `createTask:${teamName}:prompt` });
|
||||||
const [prevOpen, setPrevOpen] = useState(false);
|
const [prevOpen, setPrevOpen] = useState(false);
|
||||||
|
|
||||||
if (open && !prevOpen) {
|
if (open && !prevOpen) {
|
||||||
|
|
|
||||||
|
|
@ -69,19 +69,20 @@ export const SendMessageDialog = ({
|
||||||
const [pendingAutoClose, setPendingAutoClose] = useState(false);
|
const [pendingAutoClose, setPendingAutoClose] = useState(false);
|
||||||
if (open && lastResult && lastResult !== prevResult) {
|
if (open && lastResult && lastResult !== prevResult) {
|
||||||
setPrevResult(lastResult);
|
setPrevResult(lastResult);
|
||||||
|
setMember('');
|
||||||
|
setSummary('');
|
||||||
setPendingAutoClose(true);
|
setPendingAutoClose(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Side effects (onClose mutates parent state) must run in useEffect, not render phase
|
// Side effects (onClose mutates parent state) must run in useEffect, not render phase
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingAutoClose) {
|
if (pendingAutoClose) {
|
||||||
setMember('');
|
|
||||||
textDraft.clearDraft();
|
textDraft.clearDraft();
|
||||||
setSummary('');
|
|
||||||
setPendingAutoClose(false);
|
setPendingAutoClose(false);
|
||||||
onClose();
|
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[]>(
|
const mentionSuggestions = useMemo<MentionSuggestion[]>(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ function parseMentionSegments(text: string, suggestions: MentionSuggestion[]): S
|
||||||
// Character after name must be boundary
|
// Character after name must be boundary
|
||||||
if (end < text.length) {
|
if (end < text.length) {
|
||||||
const after = text[end];
|
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;
|
if (!/[\s,.:;!?\)\]\}\-]/.test(after)) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export function getCaretCoordinates(
|
||||||
mirror.style.position = 'absolute';
|
mirror.style.position = 'absolute';
|
||||||
mirror.style.visibility = 'hidden';
|
mirror.style.visibility = 'hidden';
|
||||||
mirror.style.whiteSpace = 'pre-wrap';
|
mirror.style.whiteSpace = 'pre-wrap';
|
||||||
mirror.style.wordWrap = 'break-word';
|
mirror.style.overflowWrap = 'break-word';
|
||||||
mirror.style.overflow = 'hidden';
|
mirror.style.overflow = 'hidden';
|
||||||
|
|
||||||
for (const prop of MIRROR_PROPS) {
|
for (const prop of MIRROR_PROPS) {
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,21 @@ export const AGENT_BLOCK_OPEN = '```' + AGENT_BLOCK_TAG;
|
||||||
export const AGENT_BLOCK_CLOSE = '```';
|
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.
|
* Supports optional leading/trailing whitespace and newlines around the block.
|
||||||
*/
|
*/
|
||||||
export const AGENT_BLOCK_REGEX = new RegExp(
|
const AGENT_BLOCK_PATTERN = '\\n?```' + AGENT_BLOCK_TAG + '\\n[\\s\\S]*?\\n```\\n?';
|
||||||
'\\n?```' + AGENT_BLOCK_TAG + '\\n[\\s\\S]*?\\n```\\n?',
|
|
||||||
'g'
|
/**
|
||||||
);
|
* 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');
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,9 @@ import {
|
||||||
TEAM_PROVISIONING_STATUS,
|
TEAM_PROVISIONING_STATUS,
|
||||||
TEAM_REQUEST_REVIEW,
|
TEAM_REQUEST_REVIEW,
|
||||||
TEAM_SEND_MESSAGE,
|
TEAM_SEND_MESSAGE,
|
||||||
|
TEAM_GET_ALL_TASKS,
|
||||||
TEAM_GET_MEMBER_LOGS,
|
TEAM_GET_MEMBER_LOGS,
|
||||||
|
TEAM_GET_MEMBER_STATS,
|
||||||
TEAM_START_TASK,
|
TEAM_START_TASK,
|
||||||
TEAM_UPDATE_CONFIG,
|
TEAM_UPDATE_CONFIG,
|
||||||
TEAM_UPDATE_KANBAN,
|
TEAM_UPDATE_KANBAN,
|
||||||
|
|
@ -125,7 +127,9 @@ describe('ipc teams handlers', () => {
|
||||||
expect(handlers.has(TEAM_ALIVE_LIST)).toBe(true);
|
expect(handlers.has(TEAM_ALIVE_LIST)).toBe(true);
|
||||||
expect(handlers.has(TEAM_CREATE_CONFIG)).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_LOGS)).toBe(true);
|
||||||
|
expect(handlers.has(TEAM_GET_MEMBER_STATS)).toBe(true);
|
||||||
expect(handlers.has(TEAM_UPDATE_CONFIG)).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 () => {
|
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_PROCESS_ALIVE)).toBe(false);
|
||||||
expect(handlers.has(TEAM_ALIVE_LIST)).toBe(false);
|
expect(handlers.has(TEAM_ALIVE_LIST)).toBe(false);
|
||||||
expect(handlers.has(TEAM_CREATE_CONFIG)).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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue