feat: optimize team display name resolution and enhance file change handling
- Introduced a caching mechanism for team display names to reduce redundant API calls and improve performance. - Updated file change event handling to debounce cache invalidation, preventing unnecessary rescans during rapid file changes. - Enhanced GlobalTaskDetailDialog and TaskDetailDialog to manage loading states and improve team data fetching logic. - Refactored teamSlice to streamline team selection and data loading processes.
This commit is contained in:
parent
ae2f7d9570
commit
b346fd319d
5 changed files with 206 additions and 90 deletions
|
|
@ -62,6 +62,7 @@ import {
|
||||||
UpdaterService,
|
UpdaterService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
|
import type { FileChangeEvent } from '@main/types';
|
||||||
import type { TeamChangeEvent } from '@shared/types';
|
import type { TeamChangeEvent } from '@shared/types';
|
||||||
|
|
||||||
const logger = createLogger('App');
|
const logger = createLogger('App');
|
||||||
|
|
@ -76,18 +77,51 @@ const INBOX_NOTIFY_DEBOUNCE_MS = 500;
|
||||||
/** Messages sent from our UI (user_sent) — suppress notifications for these. */
|
/** Messages sent from our UI (user_sent) — suppress notifications for these. */
|
||||||
const suppressedSources = new Set(['user_sent']);
|
const suppressedSources = new Set(['user_sent']);
|
||||||
|
|
||||||
|
// --- Team display name cache (avoid listTeams() on every notification) ---
|
||||||
|
const TEAM_DISPLAY_NAME_TTL_MS = 30_000;
|
||||||
|
const teamDisplayNameCache = new Map<string, { value: string; expiresAt: number }>();
|
||||||
|
let teamListInFlight: Promise<Map<string, string>> | null = null;
|
||||||
|
|
||||||
|
async function refreshTeamDisplayNameCache(): Promise<Map<string, string>> {
|
||||||
|
if (teamListInFlight) {
|
||||||
|
return teamListInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
teamListInFlight = (async () => {
|
||||||
|
const out = new Map<string, string>();
|
||||||
|
try {
|
||||||
|
if (!teamDataService) return out;
|
||||||
|
const summary = await teamDataService.listTeams();
|
||||||
|
for (const team of summary) {
|
||||||
|
if (team?.teamName) {
|
||||||
|
out.set(team.teamName, team.displayName || team.teamName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
teamListInFlight = null;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return teamListInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolve human-friendly team display name, falling back to raw teamName. */
|
/** Resolve human-friendly team display name, falling back to raw teamName. */
|
||||||
async function resolveTeamDisplayName(teamName: string): Promise<string> {
|
async function resolveTeamDisplayName(teamName: string): Promise<string> {
|
||||||
try {
|
const cached = teamDisplayNameCache.get(teamName);
|
||||||
if (teamDataService) {
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
const summary = await teamDataService.listTeams();
|
return cached.value;
|
||||||
const team = summary.find((t) => t.teamName === teamName);
|
|
||||||
if (team?.displayName) return team.displayName;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fallback
|
|
||||||
}
|
}
|
||||||
return teamName;
|
|
||||||
|
const map = await refreshTeamDisplayNameCache();
|
||||||
|
const resolved = map.get(teamName) ?? teamName;
|
||||||
|
teamDisplayNameCache.set(teamName, {
|
||||||
|
value: resolved,
|
||||||
|
expiresAt: Date.now() + TEAM_DISPLAY_NAME_TTL_MS,
|
||||||
|
});
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function notifyNewInboxMessages(teamName: string, detail: string): Promise<void> {
|
async function notifyNewInboxMessages(teamName: string, detail: string): Promise<void> {
|
||||||
|
|
@ -220,9 +254,37 @@ function wireFileWatcherEvents(context: ServiceContext): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire file-change events to renderer and HTTP SSE
|
// Wire file-change events to renderer and HTTP SSE
|
||||||
|
const SCAN_CACHE_INVALIDATE_DEBOUNCE_MS = 250;
|
||||||
|
let scanCacheInvalidateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const scheduleScanCacheInvalidation = (): void => {
|
||||||
|
if (scanCacheInvalidateTimer) {
|
||||||
|
clearTimeout(scanCacheInvalidateTimer);
|
||||||
|
}
|
||||||
|
scanCacheInvalidateTimer = setTimeout(() => {
|
||||||
|
scanCacheInvalidateTimer = null;
|
||||||
|
context.projectScanner.clearScanCache();
|
||||||
|
}, SCAN_CACHE_INVALIDATE_DEBOUNCE_MS);
|
||||||
|
};
|
||||||
|
|
||||||
const fileChangeHandler = (event: unknown): void => {
|
const fileChangeHandler = (event: unknown): void => {
|
||||||
// Invalidate the scan cache so the next IPC request sees fresh data
|
// Avoid triggering a full project rescan on every session append.
|
||||||
context.projectScanner.clearScanCache();
|
// The ProjectScanner already has a short TTL cache; we only invalidate for
|
||||||
|
// structural changes (add/unlink), and we debounce bursts of events.
|
||||||
|
try {
|
||||||
|
if (event && typeof event === 'object') {
|
||||||
|
const row = event as Partial<FileChangeEvent>;
|
||||||
|
const isSubagent = row.isSubagent === true;
|
||||||
|
const changeType = row.type;
|
||||||
|
if (!isSubagent && (changeType === 'add' || changeType === 'unlink')) {
|
||||||
|
scheduleScanCacheInvalidation();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: if we can't classify the event, invalidate (debounced).
|
||||||
|
scheduleScanCacheInvalidation();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('file-change', event);
|
mainWindow.webContents.send('file-change', event);
|
||||||
|
|
@ -230,7 +292,13 @@ function wireFileWatcherEvents(context: ServiceContext): void {
|
||||||
httpServer?.broadcast('file-change', event);
|
httpServer?.broadcast('file-change', event);
|
||||||
};
|
};
|
||||||
context.fileWatcher.on('file-change', fileChangeHandler);
|
context.fileWatcher.on('file-change', fileChangeHandler);
|
||||||
fileChangeCleanup = () => context.fileWatcher.off('file-change', fileChangeHandler);
|
fileChangeCleanup = () => {
|
||||||
|
context.fileWatcher.off('file-change', fileChangeHandler);
|
||||||
|
if (scanCacheInvalidateTimer) {
|
||||||
|
clearTimeout(scanCacheInvalidateTimer);
|
||||||
|
scanCacheInvalidateTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Forward checklist-change events to renderer and HTTP SSE (mirrors file-change pattern above)
|
// Forward checklist-change events to renderer and HTTP SSE (mirrors file-change pattern above)
|
||||||
const todoChangeHandler = (event: unknown): void => {
|
const todoChangeHandler = (event: unknown): void => {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import { createLogger } from '@shared/utils/logger';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as readline from 'readline';
|
|
||||||
|
|
||||||
import { gitIdentityResolver } from '../parsing/GitIdentityResolver';
|
import { gitIdentityResolver } from '../parsing/GitIdentityResolver';
|
||||||
|
|
||||||
|
|
@ -969,58 +968,76 @@ export class TeamDataService {
|
||||||
|
|
||||||
const leadName = config.members?.find((m) => m.agentType === 'team-lead')?.name ?? 'team-lead';
|
const leadName = config.members?.find((m) => m.agentType === 'team-lead')?.name ?? 'team-lead';
|
||||||
|
|
||||||
const texts: InboxMessage[] = [];
|
// Optimization: read from the end of the JSONL file (we only need the last N texts).
|
||||||
|
// The full file can be huge; scanning from the start causes long stalls on Windows.
|
||||||
const stream = fs.createReadStream(jsonlPath, { encoding: 'utf8' });
|
const MAX_SCAN_BYTES = 8 * 1024 * 1024; // 8MB tail cap
|
||||||
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
const INITIAL_SCAN_BYTES = 256 * 1024; // 256KB
|
||||||
|
|
||||||
|
const textsReversed: InboxMessage[] = [];
|
||||||
|
const handle = await fs.promises.open(jsonlPath, 'r');
|
||||||
try {
|
try {
|
||||||
for await (const line of rl) {
|
const stat = await handle.stat();
|
||||||
const trimmed = line.trim();
|
const fileSize = stat.size;
|
||||||
if (!trimmed) continue;
|
|
||||||
|
|
||||||
let msg: Record<string, unknown>;
|
let scanBytes = Math.min(INITIAL_SCAN_BYTES, fileSize);
|
||||||
try {
|
while (textsReversed.length < MAX_LEAD_TEXTS && scanBytes <= MAX_SCAN_BYTES) {
|
||||||
msg = JSON.parse(trimmed) as Record<string, unknown>;
|
const start = Math.max(0, fileSize - scanBytes);
|
||||||
} catch {
|
const buffer = Buffer.alloc(scanBytes);
|
||||||
continue;
|
await handle.read(buffer, 0, scanBytes, start);
|
||||||
|
const chunk = buffer.toString('utf8');
|
||||||
|
|
||||||
|
const lines = chunk.split(/\r?\n/);
|
||||||
|
// If we started mid-file, the first line may be partial — drop it.
|
||||||
|
const fromIndex = start > 0 ? 1 : 0;
|
||||||
|
|
||||||
|
for (let i = lines.length - 1; i >= fromIndex; i--) {
|
||||||
|
const trimmed = lines[i]?.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
let msg: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(trimmed) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type !== 'assistant') continue;
|
||||||
|
|
||||||
|
const message = (msg.message ?? msg) as Record<string, unknown>;
|
||||||
|
const content = message.content;
|
||||||
|
if (!Array.isArray(content)) continue;
|
||||||
|
|
||||||
|
const timestamp =
|
||||||
|
typeof msg.timestamp === 'string' ? msg.timestamp : new Date().toISOString();
|
||||||
|
|
||||||
|
for (const block of content as Record<string, unknown>[]) {
|
||||||
|
if (block.type !== 'text' || typeof block.text !== 'string') continue;
|
||||||
|
const text = block.text.trim();
|
||||||
|
if (text.length < MIN_TEXT_LENGTH) continue;
|
||||||
|
textsReversed.push({
|
||||||
|
from: leadName,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
read: true,
|
||||||
|
source: 'lead_session',
|
||||||
|
});
|
||||||
|
if (textsReversed.length >= MAX_LEAD_TEXTS) break;
|
||||||
|
}
|
||||||
|
if (textsReversed.length >= MAX_LEAD_TEXTS) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type !== 'assistant') continue;
|
if (textsReversed.length >= MAX_LEAD_TEXTS) break;
|
||||||
|
if (scanBytes === fileSize) break;
|
||||||
const message = (msg.message ?? msg) as Record<string, unknown>;
|
scanBytes = Math.min(fileSize, scanBytes * 2);
|
||||||
const content = message.content;
|
|
||||||
if (!Array.isArray(content)) continue;
|
|
||||||
|
|
||||||
const timestamp =
|
|
||||||
typeof msg.timestamp === 'string' ? msg.timestamp : new Date().toISOString();
|
|
||||||
|
|
||||||
for (const block of content as Record<string, unknown>[]) {
|
|
||||||
if (block.type !== 'text' || typeof block.text !== 'string') continue;
|
|
||||||
|
|
||||||
const text = block.text.trim();
|
|
||||||
if (text.length < MIN_TEXT_LENGTH) continue;
|
|
||||||
|
|
||||||
texts.push({
|
|
||||||
from: leadName,
|
|
||||||
text,
|
|
||||||
timestamp,
|
|
||||||
read: true,
|
|
||||||
source: 'lead_session',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
await handle.close();
|
||||||
stream.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep only the last N texts
|
// Convert back to chronological order (old behavior) and keep the last N texts.
|
||||||
if (texts.length > MAX_LEAD_TEXTS) {
|
textsReversed.reverse();
|
||||||
return texts.slice(-MAX_LEAD_TEXTS);
|
const texts = textsReversed;
|
||||||
}
|
return texts.length > MAX_LEAD_TEXTS ? texts.slice(-MAX_LEAD_TEXTS) : texts;
|
||||||
|
|
||||||
return texts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateKanban(teamName: string, taskId: string, patch: UpdateKanbanPatch): Promise<void> {
|
async updateKanban(teamName: string, taskId: string, patch: UpdateKanbanPatch): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
|
@ -6,7 +6,7 @@ import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { TaskDetailDialog } from './TaskDetailDialog';
|
import { TaskDetailDialog } from './TaskDetailDialog';
|
||||||
|
|
||||||
import type { TeamTaskWithKanban } from '@shared/types';
|
import type { GlobalTask, TeamTaskWithKanban } from '@shared/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global wrapper around TaskDetailDialog.
|
* Global wrapper around TaskDetailDialog.
|
||||||
|
|
@ -17,36 +17,59 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
|
||||||
const {
|
const {
|
||||||
globalTaskDetail,
|
globalTaskDetail,
|
||||||
closeGlobalTaskDetail,
|
closeGlobalTaskDetail,
|
||||||
|
selectedTeamName,
|
||||||
selectedTeamData,
|
selectedTeamData,
|
||||||
selectedTeamLoading,
|
selectedTeamLoading,
|
||||||
|
selectTeam,
|
||||||
openTeamTab,
|
openTeamTab,
|
||||||
setPendingReviewRequest,
|
setPendingReviewRequest,
|
||||||
|
globalTasks,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
globalTaskDetail: s.globalTaskDetail,
|
globalTaskDetail: s.globalTaskDetail,
|
||||||
closeGlobalTaskDetail: s.closeGlobalTaskDetail,
|
closeGlobalTaskDetail: s.closeGlobalTaskDetail,
|
||||||
|
selectedTeamName: s.selectedTeamName,
|
||||||
selectedTeamData: s.selectedTeamData,
|
selectedTeamData: s.selectedTeamData,
|
||||||
selectedTeamLoading: s.selectedTeamLoading,
|
selectedTeamLoading: s.selectedTeamLoading,
|
||||||
|
selectTeam: s.selectTeam,
|
||||||
openTeamTab: s.openTeamTab,
|
openTeamTab: s.openTeamTab,
|
||||||
setPendingReviewRequest: s.setPendingReviewRequest,
|
setPendingReviewRequest: s.setPendingReviewRequest,
|
||||||
|
globalTasks: s.globalTasks,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const taskMap = useMemo(() => {
|
|
||||||
const map = new Map<string, TeamTaskWithKanban>();
|
|
||||||
if (!selectedTeamData) return map;
|
|
||||||
for (const t of selectedTeamData.tasks) map.set(t.id, t);
|
|
||||||
return map;
|
|
||||||
}, [selectedTeamData]);
|
|
||||||
|
|
||||||
const activeMembers = useMemo(
|
|
||||||
() => selectedTeamData?.members.filter((m) => !m.removedAt) ?? [],
|
|
||||||
[selectedTeamData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const teamName = globalTaskDetail?.teamName ?? '';
|
const teamName = globalTaskDetail?.teamName ?? '';
|
||||||
const taskId = globalTaskDetail?.taskId ?? '';
|
const taskId = globalTaskDetail?.taskId ?? '';
|
||||||
|
|
||||||
|
// Load full team data in the background to enable "as before" details (logs/changes/members).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!globalTaskDetail) return;
|
||||||
|
if (selectedTeamName === teamName && selectedTeamData) return;
|
||||||
|
void selectTeam(teamName, { skipProjectAutoSelect: true });
|
||||||
|
}, [globalTaskDetail, selectTeam, selectedTeamData, selectedTeamName, teamName]);
|
||||||
|
|
||||||
|
const isFullTeamLoaded = selectedTeamName === teamName && !!selectedTeamData;
|
||||||
|
|
||||||
|
const taskMap = useMemo(() => {
|
||||||
|
const map = new Map<string, TeamTaskWithKanban>();
|
||||||
|
if (!globalTaskDetail) return map;
|
||||||
|
if (isFullTeamLoaded && selectedTeamData) {
|
||||||
|
for (const t of selectedTeamData.tasks) map.set(t.id, t);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
for (const t of globalTasks) {
|
||||||
|
if (t.teamName === globalTaskDetail.teamName) {
|
||||||
|
map.set(t.id, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [globalTaskDetail, globalTasks, isFullTeamLoaded, selectedTeamData]);
|
||||||
|
|
||||||
|
const activeMembers = useMemo(
|
||||||
|
() => (isFullTeamLoaded ? (selectedTeamData?.members.filter((m) => !m.removedAt) ?? []) : []),
|
||||||
|
[isFullTeamLoaded, selectedTeamData]
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpenTeam = useCallback((): void => {
|
const handleOpenTeam = useCallback((): void => {
|
||||||
closeGlobalTaskDetail();
|
closeGlobalTaskDetail();
|
||||||
openTeamTab(teamName, undefined, taskId);
|
openTeamTab(teamName, undefined, taskId);
|
||||||
|
|
@ -63,13 +86,16 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
|
||||||
|
|
||||||
if (!globalTaskDetail) return null;
|
if (!globalTaskDetail) return null;
|
||||||
|
|
||||||
const task = taskMap.get(taskId) ?? null;
|
const task = (taskMap.get(taskId) as GlobalTask | undefined) ?? null;
|
||||||
const kanbanTaskState = selectedTeamData?.kanbanState.tasks[taskId];
|
const kanbanTaskState = isFullTeamLoaded
|
||||||
|
? selectedTeamData?.kanbanState.tasks[taskId]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TaskDetailDialog
|
<TaskDetailDialog
|
||||||
open
|
open
|
||||||
loading={selectedTeamLoading}
|
variant={isFullTeamLoaded ? 'team' : 'global'}
|
||||||
|
loading={!isFullTeamLoaded && selectedTeamLoading}
|
||||||
task={task}
|
task={task}
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
kanbanTaskState={kanbanTaskState}
|
kanbanTaskState={kanbanTaskState}
|
||||||
|
|
@ -77,7 +103,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
|
||||||
members={activeMembers}
|
members={activeMembers}
|
||||||
onClose={closeGlobalTaskDetail}
|
onClose={closeGlobalTaskDetail}
|
||||||
onOwnerChange={undefined}
|
onOwnerChange={undefined}
|
||||||
onViewChanges={handleViewChanges}
|
onViewChanges={isFullTeamLoaded ? handleViewChanges : undefined}
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ import type { KanbanTaskState, ResolvedTeamMember, TeamTaskWithKanban } from '@s
|
||||||
interface TaskDetailDialogProps {
|
interface TaskDetailDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
variant?: 'team' | 'global';
|
||||||
task: TeamTaskWithKanban | null;
|
task: TeamTaskWithKanban | null;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
kanbanTaskState?: KanbanTaskState;
|
kanbanTaskState?: KanbanTaskState;
|
||||||
|
|
@ -79,6 +80,7 @@ interface TaskDetailDialogProps {
|
||||||
export const TaskDetailDialog = ({
|
export const TaskDetailDialog = ({
|
||||||
open,
|
open,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
variant = 'team',
|
||||||
task,
|
task,
|
||||||
teamName,
|
teamName,
|
||||||
kanbanTaskState,
|
kanbanTaskState,
|
||||||
|
|
@ -208,6 +210,7 @@ export const TaskDetailDialog = ({
|
||||||
}, [activeChangeSet, currentTask]);
|
}, [activeChangeSet, currentTask]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (variant !== 'team') return;
|
||||||
if (!open || !currentTask || !isTaskCompleted || !onViewChanges) return;
|
if (!open || !currentTask || !isTaskCompleted || !onViewChanges) return;
|
||||||
// Only fetch if we don't already have data for this task
|
// Only fetch if we don't already have data for this task
|
||||||
if (taskChangesFiles !== null) return;
|
if (taskChangesFiles !== null) return;
|
||||||
|
|
@ -220,6 +223,7 @@ export const TaskDetailDialog = ({
|
||||||
fetchTaskChanges,
|
fetchTaskChanges,
|
||||||
taskChangesFiles,
|
taskChangesFiles,
|
||||||
onViewChanges,
|
onViewChanges,
|
||||||
|
variant,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDependencyClick = (taskId: string): void => {
|
const handleDependencyClick = (taskId: string): void => {
|
||||||
|
|
@ -536,7 +540,7 @@ export const TaskDetailDialog = ({
|
||||||
</CollapsibleTeamSection>
|
</CollapsibleTeamSection>
|
||||||
|
|
||||||
{/* Changes */}
|
{/* Changes */}
|
||||||
{isTaskCompleted && onViewChanges ? (
|
{variant === 'team' && isTaskCompleted && onViewChanges ? (
|
||||||
<CollapsibleTeamSection
|
<CollapsibleTeamSection
|
||||||
title="Changes"
|
title="Changes"
|
||||||
icon={<FileDiff size={14} />}
|
icon={<FileDiff size={14} />}
|
||||||
|
|
@ -582,16 +586,22 @@ export const TaskDetailDialog = ({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Execution Logs — sessions that reference this task */}
|
{/* Execution Logs — sessions that reference this task */}
|
||||||
<CollapsibleTeamSection title="Execution Logs" icon={<ScrollText size={14} />} defaultOpen>
|
{variant === 'team' ? (
|
||||||
<div className="min-w-0 overflow-hidden">
|
<CollapsibleTeamSection
|
||||||
<MemberLogsTab
|
title="Execution Logs"
|
||||||
teamName={teamName}
|
icon={<ScrollText size={14} />}
|
||||||
taskId={currentTask.id}
|
defaultOpen
|
||||||
taskOwner={currentTask.owner}
|
>
|
||||||
taskStatus={currentTask.status}
|
<div className="min-w-0 overflow-hidden">
|
||||||
/>
|
<MemberLogsTab
|
||||||
</div>
|
teamName={teamName}
|
||||||
</CollapsibleTeamSection>
|
taskId={currentTask.id}
|
||||||
|
taskOwner={currentTask.owner}
|
||||||
|
taskStatus={currentTask.status}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTeamSection>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="mb-3 space-y-2">
|
<div className="mb-3 space-y-2">
|
||||||
{/* Dependencies */}
|
{/* Dependencies */}
|
||||||
|
|
|
||||||
|
|
@ -204,11 +204,6 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
setPendingReviewRequest: (req) => set({ pendingReviewRequest: req }),
|
setPendingReviewRequest: (req) => set({ pendingReviewRequest: req }),
|
||||||
openGlobalTaskDetail: (teamName: string, taskId: string) => {
|
openGlobalTaskDetail: (teamName: string, taskId: string) => {
|
||||||
set({ globalTaskDetail: { teamName, taskId } });
|
set({ globalTaskDetail: { teamName, taskId } });
|
||||||
// Ensure team data is loaded for the dialog
|
|
||||||
const state = get();
|
|
||||||
if (state.selectedTeamName !== teamName || !state.selectedTeamData) {
|
|
||||||
void state.selectTeam(teamName, { skipProjectAutoSelect: true });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
closeGlobalTaskDetail: () => set({ globalTaskDetail: null }),
|
closeGlobalTaskDetail: () => set({ globalTaskDetail: null }),
|
||||||
addingComment: false,
|
addingComment: false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue