mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 06:17:41 +00:00
fix: UI bug fixes — settings, cron, MCP, WS, events (#855)
* fix: UI bug fixes — team settings, cron, MCP, WS reconnect, events, config Critical: - Preserve team settings version field to prevent v2→v1 downgrade - Add blocker_escalation + slow_tool to BE whitelist struct - Cron update only sends enabled when actually changed - MCP form no longer strips mcp_ prefix from tool_prefix on edit - Prevent duplicate pairing modals on WS reconnect Medium: - Fix events category filter using wrong filtered source - Fix config behavior section leaking masked "***" token - Fix agent WS fallback mapping wrong status/type values - Fix offset=0 silently dropped in activity/vault/episodic hooks - Fix channel/provider/cron dialog useEffect missing deps - Fix mobile sidebar not closing on programmatic navigation - Fix WS role not cleared on disconnect, auth failure infinite loop - Fix SearchInput spurious calls from unstable onChange ref - Fix pending-messages poll/timeout not cleaned up on unmount - Fix SummaryBlock "Show more" button never appearing - Fix session delete double-navigate, channel delete not awaited - Fix skill view passing slug instead of name - Fix storage listFiles silently swallowing errors - Fix graph search not early-terminating on large graphs Low: - Remove unused sessionKey from handleAgentChange deps - Use ROUTES.CHAT constant in sidebar - Fix reconnect backoff off-by-one - Fix cron advanced dialog stale form on prop update - Clear vault search results on dialog close * fix: address review findings — pairing guard reset, stale closure, search cleanup - Reset pairingInProgress on disconnect() to prevent stuck pairing state - Add initial to handleSave deps to prevent stale version closure - Use handleOpenChange in vault search result select to clear stale results --------- Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com> Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
28 files changed
+90
-44
No files matched your search
@@ -312,19 +312,23 @@ func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client,
|
||||
EscalationActions []string `json:"escalation_actions,omitempty"`
|
||||
WorkspaceScope string `json:"workspace_scope,omitempty"`
|
||||
WorkspaceQuotaMB *int `json:"workspace_quota_mb,omitempty"`
|
||||
Notifications *struct {
|
||||
Notifications *struct {
|
||||
Dispatched *bool `json:"dispatched,omitempty"`
|
||||
Progress *bool `json:"progress,omitempty"`
|
||||
Failed *bool `json:"failed,omitempty"`
|
||||
Completed *bool `json:"completed,omitempty"`
|
||||
Commented *bool `json:"commented,omitempty"`
|
||||
NewTask *bool `json:"new_task,omitempty"`
|
||||
SlowTool *bool `json:"slow_tool,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
} `json:"notifications,omitempty"`
|
||||
MemberRequests *struct {
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
AutoDispatch *bool `json:"auto_dispatch,omitempty"`
|
||||
} `json:"member_requests,omitempty"`
|
||||
BlockerEscalation *struct {
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
} `json:"blocker_escalation,omitempty"`
|
||||
}
|
||||
raw, _ := json.Marshal(params.Settings)
|
||||
var access teamAccessSettings
|
||||
|
||||
@@ -21,6 +21,7 @@ export class WsClient {
|
||||
private reconnectAttempts = 0;
|
||||
private authenticated = false;
|
||||
private intentionalClose = false;
|
||||
private pairingInProgress = false;
|
||||
private connectGeneration = 0;
|
||||
|
||||
/** Server-assigned role from connect response. */
|
||||
@@ -90,6 +91,7 @@ export class WsClient {
|
||||
|
||||
disconnect(): void {
|
||||
this.intentionalClose = true;
|
||||
this.pairingInProgress = false;
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
@@ -221,10 +223,14 @@ export class WsClient {
|
||||
|
||||
// Browser pairing: server requires approval
|
||||
if (res?.status === "pending_pairing" && res.pairing_code && res.sender_id) {
|
||||
this.onPairingRequired?.(res.pairing_code, res.sender_id);
|
||||
if (!this.pairingInProgress) {
|
||||
this.pairingInProgress = true;
|
||||
this.onPairingRequired?.(res.pairing_code, res.sender_id);
|
||||
}
|
||||
// Keep connection alive for polling browser.pairing.status
|
||||
return;
|
||||
}
|
||||
this.pairingInProgress = false;
|
||||
|
||||
// Server accepted connection but assigned viewer role → token is invalid
|
||||
if (this.getToken() && res?.role === "viewer") {
|
||||
@@ -251,6 +257,7 @@ export class WsClient {
|
||||
this.onAuthFailure?.();
|
||||
return;
|
||||
}
|
||||
this.intentionalClose = true;
|
||||
this.ws?.close();
|
||||
}
|
||||
}
|
||||
@@ -329,11 +336,11 @@ export class WsClient {
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer) return;
|
||||
|
||||
this.reconnectAttempts++;
|
||||
const delay = Math.min(
|
||||
this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts),
|
||||
this.maxReconnectDelay,
|
||||
);
|
||||
this.reconnectAttempts++;
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
|
||||
@@ -166,7 +166,7 @@ export function SigmaGraphContainer({
|
||||
onSigmaReady?.(null);
|
||||
}
|
||||
};
|
||||
}, [graph, edgeType, compact]);
|
||||
}, [graph, edgeType, compact, setSigmaRef]);
|
||||
|
||||
// --- Update theme colors without re-init ---
|
||||
useEffect(() => {
|
||||
|
||||
@@ -36,13 +36,15 @@ export function SigmaGraphSearch({ sigma, graph, onNodeSelect, placeholder = "Se
|
||||
}
|
||||
const lower = q.toLowerCase();
|
||||
const matches: SearchResult[] = [];
|
||||
graph.forEachNode((node, attrs) => {
|
||||
if (matches.length >= 10) return;
|
||||
const nodes = graph.nodes();
|
||||
for (let i = 0; i < nodes.length && matches.length < 10; i++) {
|
||||
const node = nodes[i]!;
|
||||
const attrs = graph.getNodeAttributes(node);
|
||||
const label = (attrs.label as string) || "";
|
||||
if (label.toLowerCase().includes(lower)) {
|
||||
matches.push({ id: node, label, color: attrs.color as string });
|
||||
}
|
||||
});
|
||||
}
|
||||
setResults(matches);
|
||||
setOpen(matches.length > 0);
|
||||
setActiveIndex(0);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { Outlet, useLocation } from "react-router";
|
||||
import { WifiOff } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -29,6 +30,13 @@ export function AppLayout() {
|
||||
const connected = useAuthStore((s) => s.connected);
|
||||
const isMobile = useIsTablet();
|
||||
|
||||
// Close mobile sidebar on route change (e.g. programmatic navigation, back button)
|
||||
useEffect(() => {
|
||||
if (isMobile && mobileSidebarOpen) {
|
||||
setMobileSidebarOpen(false);
|
||||
}
|
||||
}, [location.pathname]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh overflow-hidden safe-top">
|
||||
{isMobile ? (
|
||||
|
||||
@@ -84,7 +84,7 @@ export function Sidebar({ collapsed, onNavItemClick }: SidebarProps) {
|
||||
<nav className="flex-1 space-y-4 overflow-y-auto px-2 py-4">
|
||||
<SidebarGroup label={t("groups.core")} collapsed={collapsed}>
|
||||
<SidebarItem to={ROUTES.OVERVIEW} icon={LayoutDashboard} label={t("nav.overview")} collapsed={collapsed} />
|
||||
<SidebarItem to="/chat" icon={MessageSquare} label={t("nav.chat")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.CHAT} icon={MessageSquare} label={t("nav.chat")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.AGENTS} icon={Bot} label={t("nav.agents")} collapsed={collapsed} />
|
||||
<SidebarItem to={ROUTES.TEAMS} icon={Users} label={t("nav.agentTeams")} collapsed={collapsed} />
|
||||
</SidebarGroup>
|
||||
|
||||
@@ -76,6 +76,7 @@ export function WsProvider({ children }: { children: React.ReactNode }) {
|
||||
});
|
||||
}
|
||||
if (state === "disconnected") {
|
||||
store.setRole("");
|
||||
store.setTenant("", "", "", false);
|
||||
store.setAvailableTenants([]);
|
||||
store.setTenantSelected(false);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Search } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDebounce } from "@/hooks/use-debounce";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
interface SearchInputProps {
|
||||
value: string;
|
||||
@@ -22,10 +22,12 @@ export function SearchInput({
|
||||
const { t } = useTranslation("common");
|
||||
const [local, setLocal] = useState(value);
|
||||
const debounced = useDebounce(local, delay);
|
||||
const onChangeRef = useRef(onChange);
|
||||
onChangeRef.current = onChange;
|
||||
|
||||
useEffect(() => {
|
||||
onChange(debounced);
|
||||
}, [debounced, onChange]);
|
||||
onChangeRef.current(debounced);
|
||||
}, [debounced]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(value);
|
||||
|
||||
@@ -42,8 +42,8 @@ export function useActivity() {
|
||||
if (filters?.action) params.action = filters.action;
|
||||
if (filters?.entity_type) params.entity_type = filters.entity_type;
|
||||
if (filters?.actor_id) params.actor_id = filters.actor_id;
|
||||
if (filters?.limit) params.limit = String(filters.limit);
|
||||
if (filters?.offset) params.offset = String(filters.offset);
|
||||
if (filters?.limit !== undefined) params.limit = String(filters.limit);
|
||||
if (filters?.offset !== undefined) params.offset = String(filters.offset);
|
||||
|
||||
const res = await http.get<ActivityListResponse>("/v1/activity", params);
|
||||
setLogs(res.logs ?? []);
|
||||
|
||||
@@ -47,9 +47,9 @@ export function useAgents() {
|
||||
max_tool_iterations: 0,
|
||||
workspace: "",
|
||||
restrict_to_workspace: false,
|
||||
agent_type: "open" as const,
|
||||
agent_type: (a as unknown as { agentType?: string }).agentType === "predefined" ? "predefined" as const : "open" as const,
|
||||
is_default: false,
|
||||
status: a.isRunning ? "running" : "idle",
|
||||
status: a.isRunning ? "active" : "inactive",
|
||||
}));
|
||||
},
|
||||
staleTime: 60_000,
|
||||
|
||||
@@ -65,7 +65,7 @@ export function ChannelAdvancedDialog({
|
||||
if (!open) return;
|
||||
setValues(deriveInitialValues(instance));
|
||||
|
||||
}, [open]);
|
||||
}, [open, instance]);
|
||||
|
||||
const handleChange = useCallback((key: string, value: unknown) => {
|
||||
setValues((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
@@ -67,7 +67,7 @@ export function ChannelDetailDialogs({
|
||||
confirmValue={instance.display_name || instance.name}
|
||||
confirmLabel={t("delete.confirmLabel")}
|
||||
onConfirm={async () => {
|
||||
onDelete({
|
||||
await onDelete({
|
||||
id: instance.id,
|
||||
name: instance.display_name || instance.name,
|
||||
});
|
||||
|
||||
@@ -125,7 +125,7 @@ export function ChatPage() {
|
||||
navigate("/chat");
|
||||
}
|
||||
},
|
||||
[navigate, sessionKey],
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const handleSend = useCallback(
|
||||
|
||||
@@ -83,9 +83,13 @@ export function BehaviorSection({ config, onPatch, saving }: Props) {
|
||||
(v: T) => { setter(v); setDirty(true); };
|
||||
|
||||
const handleSave = () => {
|
||||
// Strip masked/secret values to avoid overwriting real secrets with "***"
|
||||
const gwClean = Object.fromEntries(
|
||||
Object.entries(gw).filter(([, v]) => typeof v !== "string" || v !== "***"),
|
||||
);
|
||||
onPatch({
|
||||
gateway: {
|
||||
...gw,
|
||||
...gwClean,
|
||||
tool_status: ux.tool_status,
|
||||
block_reply: ux.block_reply,
|
||||
max_message_chars: rate.max_message_chars,
|
||||
|
||||
@@ -87,7 +87,7 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd
|
||||
reset(deriveDefaults(job));
|
||||
fetchTargets();
|
||||
|
||||
}, [open]);
|
||||
}, [open, job, reset, fetchTargets]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!onUpdate) {
|
||||
|
||||
@@ -96,18 +96,20 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
} else {
|
||||
schedule = { kind: "at" as const, atMs: job.schedule.atMs ?? Date.now() + 60000, tz: timezone !== "UTC" ? timezone : "" };
|
||||
}
|
||||
await onUpdate(job.id, {
|
||||
const patch: import("../hooks/use-cron").CronJobPatch = {
|
||||
schedule,
|
||||
message: message.trim(),
|
||||
agentId: agentId.trim() || "",
|
||||
enabled,
|
||||
deliver,
|
||||
deliverChannel: deliver ? channel.trim() || undefined : undefined,
|
||||
deliverTo: deliver ? to.trim() || undefined : undefined,
|
||||
wakeHeartbeat,
|
||||
deleteAfterRun,
|
||||
stateless,
|
||||
});
|
||||
};
|
||||
// Only include enabled in patch if it actually changed to avoid unintended toggles
|
||||
if (enabled !== job.enabled) patch.enabled = enabled;
|
||||
await onUpdate(job.id, patch);
|
||||
setEditingMessage(false);
|
||||
} catch {
|
||||
// toast shown by hook
|
||||
|
||||
@@ -58,7 +58,7 @@ export function EventsPage() {
|
||||
if (!teamId) return t("global");
|
||||
return teamMap.get(teamId) ?? teamId.slice(0, 8);
|
||||
},
|
||||
[teamMap],
|
||||
[teamMap, t],
|
||||
);
|
||||
|
||||
// Unique teams from events for filter dropdown
|
||||
@@ -118,7 +118,7 @@ export function EventsPage() {
|
||||
e.event.startsWith("team.member."),
|
||||
);
|
||||
}
|
||||
return userFilteredEvents.filter((e) => e.event.startsWith(categoryFilter));
|
||||
return chatFilteredEvents.filter((e) => e.event.startsWith(categoryFilter));
|
||||
}, [chatFilteredEvents, categoryFilter]);
|
||||
|
||||
// Auto-scroll to bottom when new events arrive
|
||||
|
||||
@@ -89,7 +89,7 @@ export function MCPFormDialog({ open, onOpenChange, server, onSubmit, onTest }:
|
||||
url: server?.url ?? "",
|
||||
headers: server?.headers ?? {},
|
||||
env: server?.env ?? {},
|
||||
toolPrefix: (server?.tool_prefix ?? "").replace(/^mcp_/, ""),
|
||||
toolPrefix: server?.tool_prefix ?? "",
|
||||
timeout: server?.timeout_sec ?? 60,
|
||||
enabled: server?.enabled ?? true,
|
||||
requireUserCreds: server?.settings?.require_user_credentials ?? false,
|
||||
@@ -160,7 +160,7 @@ export function MCPFormDialog({ open, onOpenChange, server, onSubmit, onTest }:
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : t("form.saving"));
|
||||
setError(err instanceof Error ? err.message : t("form.errors.saveFailed", "Save failed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ export function useEpisodicSummaries(agentId: string, opts: { userId?: string; l
|
||||
const params = useMemo(() => {
|
||||
const p: Record<string, string> = {};
|
||||
if (opts.userId) p.user_id = opts.userId;
|
||||
if (opts.limit) p.limit = String(opts.limit);
|
||||
if (opts.offset) p.offset = String(opts.offset);
|
||||
if (opts.limit !== undefined) p.limit = String(opts.limit);
|
||||
if (opts.offset !== undefined) p.offset = String(opts.offset);
|
||||
return p;
|
||||
}, [opts.userId, opts.limit, opts.offset]);
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ export function PendingMessagesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
loadGroups();
|
||||
return () => {
|
||||
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
|
||||
if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; }
|
||||
};
|
||||
}, [loadGroups]);
|
||||
|
||||
// Clear spinner as soon as the compacting group's has_summary becomes true
|
||||
|
||||
@@ -76,7 +76,7 @@ export function ProviderAdvancedDialog({
|
||||
setAcpPermMode(s.acpPermMode);
|
||||
setAcpWorkDir(s.acpWorkDir);
|
||||
|
||||
}, [open]);
|
||||
}, [open, provider]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
|
||||
@@ -253,7 +253,6 @@ export function SessionDetailPage({
|
||||
onConfirm={async () => {
|
||||
await onDelete(session.key);
|
||||
setConfirmDelete(false);
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -52,7 +52,12 @@ export function SummaryBlock({ text }: { text: string }) {
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setNeedsTruncation(contentRef.current.scrollHeight > SUMMARY_MAX_HEIGHT);
|
||||
// Temporarily remove max-height to measure true content height
|
||||
const el = contentRef.current;
|
||||
const prev = el.style.maxHeight;
|
||||
el.style.maxHeight = "none";
|
||||
setNeedsTruncation(el.scrollHeight > SUMMARY_MAX_HEIGHT);
|
||||
el.style.maxHeight = prev;
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
@@ -62,7 +67,7 @@ export function SummaryBlock({ text }: { text: string }) {
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="mt-1 overflow-hidden transition-[max-height] duration-200"
|
||||
style={{ maxHeight: expanded ? contentRef.current?.scrollHeight : SUMMARY_MAX_HEIGHT }}
|
||||
style={{ maxHeight: expanded ? (contentRef.current?.scrollHeight ?? "none") : SUMMARY_MAX_HEIGHT }}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function SkillTableRow({
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-left hover:underline cursor-pointer"
|
||||
onClick={() => onView(skill.slug ?? skill.name)}
|
||||
onClick={() => onView(skill.name)}
|
||||
>
|
||||
{skill.name}
|
||||
</button>
|
||||
|
||||
@@ -36,6 +36,8 @@ export function useStorage() {
|
||||
const res = await http.get<StorageListResponse>("/v1/storage/files");
|
||||
setFiles(res.files ?? []);
|
||||
setBaseDir(res.baseDir ?? "");
|
||||
} catch (err) {
|
||||
toast.error(i18next.t("storage:toast.loadFailed", "Failed to load files"), userFriendlyError(err));
|
||||
} finally {
|
||||
if (!opts?.silent) setLoading(false);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
try {
|
||||
const data = form.getValues();
|
||||
const settings: TeamAccessSettings = {};
|
||||
// Preserve version from existing settings to prevent v2→v1 downgrade
|
||||
if (initial.version !== undefined) settings.version = initial.version;
|
||||
if (data.allowUserIds.length > 0) settings.allow_user_ids = data.allowUserIds;
|
||||
if (data.denyUserIds.length > 0) settings.deny_user_ids = data.denyUserIds;
|
||||
if (data.allowChannels.length > 0) settings.allow_channels = data.allowChannels;
|
||||
@@ -109,16 +111,14 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
new_task: data.notifyNewTask,
|
||||
};
|
||||
settings.notifications = notifications;
|
||||
if (data.memberRequestsEnabled) {
|
||||
settings.member_requests = { enabled: true, auto_dispatch: data.memberRequestsAutoDispatch };
|
||||
}
|
||||
settings.member_requests = { enabled: data.memberRequestsEnabled, auto_dispatch: data.memberRequestsAutoDispatch };
|
||||
if (escalationMode) {
|
||||
settings.escalation_mode = escalationMode;
|
||||
if (escalationActions.length > 0) settings.escalation_actions = escalationActions;
|
||||
}
|
||||
settings.blocker_escalation = { enabled: data.blockerEscalationEnabled };
|
||||
if (data.followupInterval !== 30) settings.followup_interval_minutes = data.followupInterval;
|
||||
if (data.followupMaxReminders !== 0) settings.followup_max_reminders = data.followupMaxReminders;
|
||||
settings.followup_interval_minutes = data.followupInterval;
|
||||
settings.followup_max_reminders = data.followupMaxReminders;
|
||||
settings.workspace_scope = data.workspaceScope || "isolated";
|
||||
await updateTeamSettings(teamId, settings);
|
||||
onSaved();
|
||||
@@ -126,7 +126,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [teamId, form, escalationMode, escalationActions, updateTeamSettings, onSaved]);
|
||||
}, [teamId, form, initial, escalationMode, escalationActions, updateTeamSettings, onSaved]);
|
||||
|
||||
const channelOptions = CHANNEL_TYPES.map((c) => ({ value: c.value, label: c.label }));
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ export function useVaultDocuments(agentId: string, opts: VaultListOpts) {
|
||||
if (opts.scope) p.scope = opts.scope;
|
||||
if (opts.docType) p.doc_type = opts.docType;
|
||||
if (opts.teamId) p.team_id = opts.teamId;
|
||||
if (opts.limit) p.limit = String(opts.limit);
|
||||
if (opts.offset) p.offset = String(opts.offset);
|
||||
if (opts.limit !== undefined) p.limit = String(opts.limit);
|
||||
if (opts.offset !== undefined) p.offset = String(opts.offset);
|
||||
return p;
|
||||
}, [agentId, opts.scope, opts.docType, opts.teamId, opts.limit, opts.offset]);
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ export function VaultSearchDialog({ agentId, open, onOpenChange, onSelectResult
|
||||
const [results, setResults] = useState<VaultSearchResult[]>([]);
|
||||
const [searching, setSearching] = useState(false);
|
||||
|
||||
// Clear stale results when dialog closes
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
if (!v) { setQuery(""); setResults([]); }
|
||||
onOpenChange(v);
|
||||
}, [onOpenChange]);
|
||||
|
||||
const handleSearch = useCallback(async () => {
|
||||
if (!query.trim()) return;
|
||||
setSearching(true);
|
||||
@@ -39,7 +45,7 @@ export function VaultSearchDialog({ agentId, open, onOpenChange, onSelectResult
|
||||
}, [query, search]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg max-sm:inset-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("search")}</DialogTitle>
|
||||
@@ -73,7 +79,7 @@ export function VaultSearchDialog({ agentId, open, onOpenChange, onSelectResult
|
||||
className="w-full text-left p-2 rounded hover:bg-muted/50 flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
onSelectResult(r.document);
|
||||
onOpenChange(false);
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
|
||||
Reference in new issue
Block a user