diff --git a/internal/tools/context_file_interceptor.go b/internal/tools/context_file_interceptor.go index ad854ef9..6c3249d4 100644 --- a/internal/tools/context_file_interceptor.go +++ b/internal/tools/context_file_interceptor.go @@ -161,24 +161,7 @@ func (b *ContextFileInterceptor) ReadFile(ctx context.Context, path string) (str } func (b *ContextFileInterceptor) readAgentFile(ctx context.Context, agentID uuid.UUID, fileName string) (string, bool, error) { - key := agentID.String() - if files, ok := b.agentCache.Get(ctx, key); ok { - for _, f := range files { - if f.FileName == fileName { - return f.Content, true, nil - } - } - return "", true, nil // cached but file not found - } - - // Cache miss → load from DB - files, err := b.agentStore.GetAgentContextFiles(ctx, agentID) - if err != nil { - return "", true, err - } - b.agentCache.Set(ctx, key, files, b.ttl) - - for _, f := range files { + for _, f := range b.cachedAgentFiles(ctx, agentID) { if f.FileName == fileName { return f.Content, true, nil } @@ -187,35 +170,7 @@ func (b *ContextFileInterceptor) readAgentFile(ctx context.Context, agentID uuid } func (b *ContextFileInterceptor) readUserFile(ctx context.Context, agentID uuid.UUID, userID, fileName string) (string, bool, error) { - key := agentID.String() + ":" + userID - - if files, ok := b.userCache.Get(ctx, key); ok { - for _, f := range files { - if f.FileName == fileName { - return f.Content, true, nil - } - } - return "", true, nil - } - - // Cache miss → load from DB - files, err := b.agentStore.GetUserContextFiles(ctx, agentID, userID) - if err != nil { - return "", true, err - } - - // Convert to AgentContextFileData for unified cache storage - cached := make([]store.AgentContextFileData, len(files)) - for i, f := range files { - cached[i] = store.AgentContextFileData{ - AgentID: f.AgentID, - FileName: f.FileName, - Content: f.Content, - } - } - b.userCache.Set(ctx, key, cached, b.ttl) - - for _, f := range files { + for _, f := range b.cachedUserFiles(ctx, agentID, userID) { if f.FileName == fileName { return f.Content, true, nil } @@ -337,13 +292,11 @@ func (b *ContextFileInterceptor) WriteFile(ctx context.Context, path, content st // LoadContextFiles loads context files for a specific user+agent combination. // Used by the agent loop to dynamically resolve context files for system prompt. +// Uses the same agentCache/userCache as ReadFile — invalidated on WriteFile and pubsub events. func (b *ContextFileInterceptor) LoadContextFiles(ctx context.Context, agentID uuid.UUID, userID, agentType string) []bootstrap.ContextFile { // Open agent: all files from user_context_files if agentType == store.AgentTypeOpen && userID != "" { - files, err := b.agentStore.GetUserContextFiles(ctx, agentID, userID) - if err != nil { - return nil - } + files := b.cachedUserFiles(ctx, agentID, userID) var result []bootstrap.ContextFile for _, f := range files { if f.Content == "" { @@ -362,11 +315,8 @@ func (b *ContextFileInterceptor) LoadContextFiles(ctx context.Context, agentID u // Predefined agent: agent files + override USER.md from user if agentType == store.AgentTypePredefined && userID != "" { - agentFiles, err := b.agentStore.GetAgentContextFiles(ctx, agentID) - if err != nil { - return nil - } - userFiles, _ := b.agentStore.GetUserContextFiles(ctx, agentID, userID) + agentFiles := b.cachedAgentFiles(ctx, agentID) + userFiles := b.cachedUserFiles(ctx, agentID, userID) // Build user file map for override lookup userMap := make(map[string]string, len(userFiles)) @@ -411,10 +361,7 @@ func (b *ContextFileInterceptor) LoadContextFiles(ctx context.Context, agentID u } // Fallback: agent-level only - agentFiles, err := b.agentStore.GetAgentContextFiles(ctx, agentID) - if err != nil { - return nil - } + agentFiles := b.cachedAgentFiles(ctx, agentID) var result []bootstrap.ContextFile for _, f := range agentFiles { if f.Content == "" { @@ -428,6 +375,45 @@ func (b *ContextFileInterceptor) LoadContextFiles(ctx context.Context, agentID u return result } +// cachedAgentFiles returns agent-level context files, using agentCache. +// Same cache used by readAgentFile — invalidated by WriteFile and pubsub events. +func (b *ContextFileInterceptor) cachedAgentFiles(ctx context.Context, agentID uuid.UUID) []store.AgentContextFileData { + key := agentID.String() + if files, ok := b.agentCache.Get(ctx, key); ok { + return files + } + files, err := b.agentStore.GetAgentContextFiles(ctx, agentID) + if err != nil { + return nil + } + b.agentCache.Set(ctx, key, files, b.ttl) + return files +} + +// cachedUserFiles returns user-level context files, using userCache. +// Same cache used by readUserFile — invalidated by WriteFile and pubsub events. +func (b *ContextFileInterceptor) cachedUserFiles(ctx context.Context, agentID uuid.UUID, userID string) []store.AgentContextFileData { + key := agentID.String() + ":" + userID + if files, ok := b.userCache.Get(ctx, key); ok { + return files + } + files, err := b.agentStore.GetUserContextFiles(ctx, agentID, userID) + if err != nil { + return nil + } + // Convert to AgentContextFileData for unified cache storage + cached := make([]store.AgentContextFileData, len(files)) + for i, f := range files { + cached[i] = store.AgentContextFileData{ + AgentID: f.AgentID, + FileName: f.FileName, + Content: f.Content, + } + } + b.userCache.Set(ctx, key, cached, b.ttl) + return cached +} + // InvalidateAgent clears the cache for a specific agent (called from event handler). func (b *ContextFileInterceptor) InvalidateAgent(agentID uuid.UUID) { ctx := context.Background() diff --git a/ui/web/src/pages/teams/task-sections/task-detail-dialog.tsx b/ui/web/src/pages/teams/task-sections/task-detail-dialog.tsx index 7616dcdc..44f26326 100644 --- a/ui/web/src/pages/teams/task-sections/task-detail-dialog.tsx +++ b/ui/web/src/pages/teams/task-sections/task-detail-dialog.tsx @@ -4,6 +4,7 @@ import { } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { ConfirmDialog } from "@/components/shared/confirm-dialog"; import { Trash2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import { formatDate } from "@/lib/format"; @@ -34,6 +35,8 @@ export function TaskDetailDialog({ const { t } = useTranslation("teams"); const [events, setEvents] = useState([]); const [attachments, setAttachments] = useState([]); + const [confirmDelete, setConfirmDelete] = useState(false); + const [deleting, setDeleting] = useState(false); const loadDetail = useCallback(async () => { try { @@ -53,13 +56,16 @@ export function TaskDetailDialog({ const handleDelete = async () => { if (!deleteTask) return; - if (!window.confirm(t("tasks.deleteConfirm"))) return; + setDeleting(true); try { await deleteTask(teamId, task.id); toast.success(t("toast.taskDeleted")); onClose(); } catch { toast.error(t("toast.failedDeleteTask")); + } finally { + setDeleting(false); + setConfirmDelete(false); } }; @@ -70,19 +76,12 @@ export function TaskDetailDialog({ onClose()}> -
- - {task.identifier && ( - {task.identifier} - )} - {t("tasks.detail.title")} - - {canDelete && ( - + + {task.identifier && ( + {task.identifier} )} -
+ {t("tasks.detail.title")} +
@@ -235,6 +234,26 @@ export function TaskDetailDialog({ )}
+ + {canDelete && ( +
+ +
+ )} + +
);