refactor: streamline team data service and enhance change review logic

- Removed unnecessary member metadata retrieval in TeamDataService to simplify task handling and prevent self-notifications for lead-owned tasks.
- Updated lead owner validation to ensure consistent case handling.
- Introduced a new reference in ChangeReviewDialog to manage in-flight file operations, preventing duplicate actions during file rejection.
- Enhanced changeReviewSlice to return null for invalid states, improving error handling and state management.

Made-with: Cursor
This commit is contained in:
iliya 2026-03-04 01:01:19 +02:00
parent dae8c50e4c
commit 2a187894a5
3 changed files with 30 additions and 47 deletions

View file

@ -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(

View file

@ -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<Record<string, number>>({});
const hunkDecisionUndoStackRef = useRef<Record<string, number[]>>({});
const newFileApplyInFlightRef = useRef(new Set<string>());
// Proxy ref for useDiffNavigation (points to active file's editor)
const activeEditorViewRef = useRef<EditorView | null>(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);
}
},
[

View file

@ -727,10 +727,10 @@ export const createChangeReviewSlice: StateCreator<AppState, [], [], ChangeRevie
memberName?: string
) => {
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<number, HunkDecision> = {};
@ -743,7 +743,7 @@ export const createChangeReviewSlice: StateCreator<AppState, [], [], ChangeRevie
const hasRejected =
fileDecision === 'rejected' || Object.values(hunkDecs).some((d) => d === 'rejected');
if (!hasRejected) return;
if (!hasRejected) return null;
try {
const content = fileContents[filePath];