diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index be40b631..cedd89bd 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -914,11 +914,10 @@ export class TeamDataService { const comment = await this.taskWriter.addComment(teamName, taskId, text); try { - const [tasks, toolPath, config, metaMembers] = await Promise.all([ + const [tasks, toolPath, config] = await Promise.all([ this.taskReader.getTasks(teamName), this.toolsInstaller.ensureInstalled(), this.configReader.getConfig(teamName).catch(() => null), - this.membersMetaStore.getMembers(teamName).catch(() => []), ]); const task = tasks.find((t) => t.id === taskId); const leadName = this.resolveLeadNameFromConfig(config); @@ -930,12 +929,9 @@ export class TeamDataService { } if (task?.owner) { - // Solo team UX: if the user comments on a lead-owned task, don't echo the - // comment back as an inbox notification from the lead. The comment is already visible. - if ( - this.isSoloTeamFromMembers(config, metaMembers, leadName) && - this.isLeadOwner(task.owner, leadName) - ) { + // UX: don't echo a user comment as an inbox notification "from the lead" when the + // task is already owned by the lead. This creates confusing self-notifications. + if (this.isLeadOwner(task.owner, leadName)) { return comment; } @@ -980,30 +976,9 @@ export class TeamDataService { } private isLeadOwner(owner: string, leadName: string): boolean { - const normalized = owner.trim(); + const normalized = owner.trim().toLowerCase(); if (!normalized) return false; - return normalized === leadName || normalized === 'team-lead'; - } - - private isSoloTeamFromMembers( - config: TeamConfig | null, - metaMembers: TeamMember[], - leadName: string - ): boolean { - const configMembers = config?.members ?? []; - const combined = [...configMembers, ...(metaMembers ?? [])]; - - const activeNonLead = combined.filter((m) => { - const name = m.name?.trim(); - if (!name) return false; - if (m.removedAt) return false; - if (m.agentType === 'team-lead') return false; - if (name === 'team-lead') return false; - if (name === leadName) return false; - return true; - }); - - return activeNonLead.length === 0; + return normalized === leadName.trim().toLowerCase() || normalized === 'team-lead'; } async sendDirectToLead( diff --git a/src/renderer/components/team/review/ChangeReviewDialog.tsx b/src/renderer/components/team/review/ChangeReviewDialog.tsx index c94b27c6..6786d5b4 100644 --- a/src/renderer/components/team/review/ChangeReviewDialog.tsx +++ b/src/renderer/components/team/review/ChangeReviewDialog.tsx @@ -115,6 +115,7 @@ export const ChangeReviewDialog = ({ // Track recent per-hunk actions so Ctrl/Cmd+Z can clear persisted decisions (reopen-safe) const lastHunkActionAtRef = useRef>({}); const hunkDecisionUndoStackRef = useRef>({}); + const newFileApplyInFlightRef = useRef(new Set()); // Proxy ref for useDiffNavigation (points to active file's editor) const activeEditorViewRef = useRef(null); @@ -230,21 +231,28 @@ export const ChangeReviewDialog = ({ const handleRejectNewFile = useCallback( async (filePath: string) => { - // Mark rejected in store + update CM view immediately for feedback - rejectAllFile(filePath); - const view = editorViewMapRef.current.get(filePath); - if (view) { - requestAnimationFrame(() => rejectAllChunks(view)); - } + if (newFileApplyInFlightRef.current.has(filePath)) return; + newFileApplyInFlightRef.current.add(filePath); + try { + // Mark rejected in store + update CM view immediately for feedback + rejectAllFile(filePath); + const view = editorViewMapRef.current.get(filePath); + if (view) { + requestAnimationFrame(() => rejectAllChunks(view)); + } - // Always apply immediately: rejecting a NEW file means deleting it from disk. - const isNew = activeChangeSet?.files.find((f) => f.filePath === filePath)?.isNewFile ?? false; - if (!isNew) return; + // Always apply immediately: rejecting a NEW file means deleting it from disk. + const isNew = + activeChangeSet?.files.find((f) => f.filePath === filePath)?.isNewFile ?? false; + if (!isNew) return; - const result = await applySingleFileDecision(teamName, filePath, taskId, memberName); - const hasErrorForFile = !!result?.errors.some((e) => e.filePath === filePath); - if (result && !hasErrorForFile) { - removeReviewFile(filePath); + const result = await applySingleFileDecision(teamName, filePath, taskId, memberName); + const hasErrorForFile = !!result?.errors.some((e) => e.filePath === filePath); + if (result && !hasErrorForFile) { + removeReviewFile(filePath); + } + } finally { + newFileApplyInFlightRef.current.delete(filePath); } }, [ diff --git a/src/renderer/store/slices/changeReviewSlice.ts b/src/renderer/store/slices/changeReviewSlice.ts index dbd4de05..45191888 100644 --- a/src/renderer/store/slices/changeReviewSlice.ts +++ b/src/renderer/store/slices/changeReviewSlice.ts @@ -727,10 +727,10 @@ export const createChangeReviewSlice: StateCreator { const { hunkDecisions, fileDecisions, fileChunkCounts, activeChangeSet, fileContents } = get(); - if (!activeChangeSet) return; + if (!activeChangeSet) return null; const file = activeChangeSet.files.find((f) => f.filePath === filePath); - if (!file) return; + if (!file) return null; const fileDecision = fileDecisions[filePath] ?? 'pending'; const hunkDecs: Record = {}; @@ -743,7 +743,7 @@ export const createChangeReviewSlice: StateCreator d === 'rejected'); - if (!hasRejected) return; + if (!hasRejected) return null; try { const content = fileContents[filePath];