mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 00:16:38 +00:00
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:
1 parent
ca44b7279f
commit
fe68bd86bc
2 files changed
+78
-73
No files matched your search
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user