perf(tools): cache LoadContextFiles + fix task detail dialog UX

- Cache agent/user context files via existing agentCache/userCache (TTL 5min)
- Extract cachedAgentFiles/cachedUserFiles helpers, DRY readAgentFile/readUserFile
- Move delete button away from close in task detail modal
- Replace native window.confirm with ConfirmDialog component
This commit is contained in:
viettranx committed 2026-03-17 09:25:37 +07:00
1 parent ca44b7279f
commit fe68bd86bc
2 files changed
+78 -73

No files matched your search

+46 -60
View File
@@ -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()
@@ -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<TeamTaskEvent[]>([]);
const [attachments, setAttachments] = useState<TeamTaskAttachment[]>([]);
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({
<Dialog open onOpenChange={() => onClose()}>
<DialogContent className="max-h-[85vh] w-[95vw] flex flex-col sm:max-w-4xl">
<DialogHeader>
<div className="flex items-center justify-between gap-2">
<DialogTitle className="flex items-center gap-2">
{task.identifier && (
<Badge variant="outline" className="font-mono text-xs">{task.identifier}</Badge>
)}
{t("tasks.detail.title")}
</DialogTitle>
{canDelete && (
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:bg-destructive/10" onClick={handleDelete}>
<Trash2 className="h-4 w-4" />
</Button>
<DialogTitle className="flex items-center gap-2">
{task.identifier && (
<Badge variant="outline" className="font-mono text-xs">{task.identifier}</Badge>
)}
</div>
{t("tasks.detail.title")}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 overflow-y-auto min-h-0 -mx-4 px-4 sm:-mx-6 sm:px-6">
@@ -235,6 +234,26 @@ export function TaskDetailDialog({
)}
</div>
{canDelete && (
<div className="flex justify-end border-t pt-3">
<Button variant="destructive" size="sm" onClick={() => setConfirmDelete(true)}>
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
{t("tasks.delete")}
</Button>
</div>
)}
<ConfirmDialog
open={confirmDelete}
onOpenChange={setConfirmDelete}
title={t("tasks.delete")}
description={t("tasks.deleteConfirm")}
confirmLabel={t("tasks.delete")}
variant="destructive"
onConfirm={handleDelete}
loading={deleting}
/>
</DialogContent>
</Dialog>
);