From c06d9d9705aef6c7a9dc6d344ec1186198d2e035 Mon Sep 17 00:00:00 2001 From: Luan Vu Date: Mon, 13 Apr 2026 08:53:26 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20UI=20bug=20fixes=20=E2=80=94=20settings,?= =?UTF-8?q?=20cron,=20MCP,=20WS,=20events=20(#855)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- internal/gateway/methods/teams_crud.go | 6 +++++- ui/web/src/api/ws-client.ts | 11 +++++++++-- .../src/components/graph/sigma-graph-container.tsx | 2 +- ui/web/src/components/graph/sigma-graph-search.tsx | 8 +++++--- ui/web/src/components/layout/app-layout.tsx | 8 ++++++++ ui/web/src/components/layout/sidebar.tsx | 2 +- ui/web/src/components/providers/ws-provider.tsx | 1 + ui/web/src/components/shared/search-input.tsx | 8 +++++--- ui/web/src/pages/activity/hooks/use-activity.ts | 4 ++-- ui/web/src/pages/agents/hooks/use-agents.ts | 4 ++-- .../channel-detail/channel-advanced-dialog.tsx | 2 +- .../channel-detail/channel-detail-dialogs.tsx | 2 +- ui/web/src/pages/chat/chat-page.tsx | 2 +- .../src/pages/config/sections/behavior-section.tsx | 6 +++++- .../pages/cron/cron-detail/cron-advanced-dialog.tsx | 2 +- .../src/pages/cron/cron-detail/cron-overview-tab.tsx | 8 +++++--- ui/web/src/pages/events/events-page.tsx | 4 ++-- ui/web/src/pages/mcp/mcp-form-dialog.tsx | 4 ++-- ui/web/src/pages/memory/hooks/use-episodic.ts | 4 ++-- .../pages/pending-messages/pending-messages-page.tsx | 4 ++++ .../provider-detail/provider-advanced-dialog.tsx | 2 +- ui/web/src/pages/sessions/session-detail-page.tsx | 1 - ui/web/src/pages/sessions/session-message-blocks.tsx | 9 +++++++-- ui/web/src/pages/skills/skill-table-row.tsx | 2 +- ui/web/src/pages/storage/hooks/use-storage.ts | 2 ++ ui/web/src/pages/teams/team-settings-tab.tsx | 12 ++++++------ ui/web/src/pages/vault/hooks/use-vault.ts | 4 ++-- ui/web/src/pages/vault/vault-search-dialog.tsx | 10 ++++++++-- 28 files changed, 90 insertions(+), 44 deletions(-) diff --git a/internal/gateway/methods/teams_crud.go b/internal/gateway/methods/teams_crud.go index d29f021a..110f813f 100644 --- a/internal/gateway/methods/teams_crud.go +++ b/internal/gateway/methods/teams_crud.go @@ -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 diff --git a/ui/web/src/api/ws-client.ts b/ui/web/src/api/ws-client.ts index 11231ae9..e8cd42dc 100644 --- a/ui/web/src/api/ws-client.ts +++ b/ui/web/src/api/ws-client.ts @@ -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; diff --git a/ui/web/src/components/graph/sigma-graph-container.tsx b/ui/web/src/components/graph/sigma-graph-container.tsx index 532a41a8..069236d3 100644 --- a/ui/web/src/components/graph/sigma-graph-container.tsx +++ b/ui/web/src/components/graph/sigma-graph-container.tsx @@ -166,7 +166,7 @@ export function SigmaGraphContainer({ onSigmaReady?.(null); } }; - }, [graph, edgeType, compact]); + }, [graph, edgeType, compact, setSigmaRef]); // --- Update theme colors without re-init --- useEffect(() => { diff --git a/ui/web/src/components/graph/sigma-graph-search.tsx b/ui/web/src/components/graph/sigma-graph-search.tsx index f6260645..ca5e96a1 100644 --- a/ui/web/src/components/graph/sigma-graph-search.tsx +++ b/ui/web/src/components/graph/sigma-graph-search.tsx @@ -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); diff --git a/ui/web/src/components/layout/app-layout.tsx b/ui/web/src/components/layout/app-layout.tsx index 361f19ba..6095d542 100644 --- a/ui/web/src/components/layout/app-layout.tsx +++ b/ui/web/src/components/layout/app-layout.tsx @@ -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 (
{isMobile ? ( diff --git a/ui/web/src/components/layout/sidebar.tsx b/ui/web/src/components/layout/sidebar.tsx index 7164bf43..5fb40f50 100644 --- a/ui/web/src/components/layout/sidebar.tsx +++ b/ui/web/src/components/layout/sidebar.tsx @@ -84,7 +84,7 @@ export function Sidebar({ collapsed, onNavItemClick }: SidebarProps) {