Skip to content

Commit ae5c64a

Browse files
committed
fix(ui): live-refresh FeatureSets and persist meta-tool activity across tabs
Two surfaces went stale when a connected MCP client drove changes through the self-management meta-tools: - FeatureSetsPage subscribed to no domain events, so `mcpmux_manage_feature_set` create/update/delete left the list stale until a manual refresh. Add a `feature-set-changed` listener that reloads the current Space. - MetaToolAuditLog ("Recent meta-tool activity") kept rows in component-local state with a `meta-tool-invoked` listener mounted only while the panel was visible — so the list vanished on tab change and showed empty if you opened it after a call had fired. Lift the rows and the listener into a global store (metaToolActivityStore) started once at app launch, so activity accumulates for the whole session regardless of which tab is mounted. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent f5c8a37 commit ae5c64a

4 files changed

Lines changed: 92 additions & 27 deletions

File tree

apps/desktop/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ServerInstallModal } from '@/components/ServerInstallModal';
88
import { SpaceSwitcher } from '@/components/SpaceSwitcher';
99
import { useDataSync } from '@/hooks/useDataSync';
1010
import { useAnalytics } from '@/hooks/useAnalytics';
11+
import { startMetaToolActivityListener } from '@/stores/metaToolActivityStore';
1112
import { initAnalytics, capture, optIn, optOut } from '@/lib/analytics';
1213
import {
1314
useAppStore,
@@ -161,6 +162,13 @@ function AppContent() {
161162
}
162163
}, [appVersion]); // eslint-disable-line react-hooks/exhaustive-deps
163164

165+
// Start the app-wide meta-tool activity listener once at launch so the
166+
// "Recent meta-tool activity" panel accumulates rows for the whole session
167+
// and survives tab changes (the listener is idempotent and app-scoped).
168+
useEffect(() => {
169+
startMetaToolActivityListener();
170+
}, []);
171+
164172
// Sync opt-in/out when user toggles analytics
165173
useEffect(() => {
166174
if (!appVersion) return;

apps/desktop/src/features/featuresets/FeatureSetsPage.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useEffect, useCallback } from 'react';
2+
import { listen } from '@tauri-apps/api/event';
23
import {
34
Plus,
45
Loader2,
@@ -101,6 +102,19 @@ export function FeatureSetsPage() {
101102
loadData(viewSpace?.id);
102103
}, [viewSpace?.id, loadData]);
103104

105+
// Refresh when a feature set changes outside this page — most importantly
106+
// the `mcpmux_manage_feature_set` meta-tool (create/update/delete) invoked by
107+
// a connected MCP client. Without this, agent-driven changes leave the list
108+
// stale until the user navigates away and back.
109+
useEffect(() => {
110+
const un = listen('feature-set-changed', () => {
111+
void loadData(viewSpace?.id);
112+
});
113+
return () => {
114+
un.then((fn) => fn()).catch(() => {});
115+
};
116+
}, [viewSpace?.id, loadData]);
117+
104118
const handleCreate = async () => {
105119
if (!createName.trim() || !viewSpace) return;
106120

apps/desktop/src/features/metaTools/MetaToolAuditLog.tsx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,20 @@
1-
import { useEffect, useState } from 'react';
2-
import { listen } from '@tauri-apps/api/event';
31
import { CheckCircle2, Eye, ShieldAlert, XCircle } from 'lucide-react';
42
import { Card, CardContent, CardHeader, CardTitle } from '@mcpmux/ui';
5-
import type { MetaToolAuditEvent } from '@/lib/api/metaTools';
6-
7-
/** Ring-buffer size — keeps the most recent N audit rows in memory. */
8-
const MAX_ROWS = 50;
3+
import {
4+
MAX_META_TOOL_ROWS as MAX_ROWS,
5+
useMetaToolActivityStore,
6+
} from '@/stores/metaToolActivityStore';
97

108
/**
11-
* In-memory audit log of every `mcpmux_*` invocation (read or write,
12-
* success or failure). Subscribes to the gateway's `meta-tool-invoked`
13-
* event channel; rows are kept only for the current UI session — the
14-
* persistent audit stream lives in the gateway's tracing logs.
9+
* Audit log of every `mcpmux_*` invocation (read or write, success or failure).
10+
*
11+
* Rows live in a global store fed by an app-level `meta-tool-invoked` listener
12+
* (see metaToolActivityStore) so they persist across tab changes and capture
13+
* calls that fired before this panel was opened. The persistent audit stream
14+
* lives in the gateway's tracing logs.
1515
*/
1616
export function MetaToolAuditLog() {
17-
const [rows, setRows] = useState<MetaToolAuditEvent[]>([]);
18-
19-
useEffect(() => {
20-
const unlisten = listen<MetaToolAuditEvent>(
21-
'meta-tool-invoked',
22-
(event) => {
23-
setRows((prev) => {
24-
// Most-recent-first; trim to MAX_ROWS.
25-
const next = [event.payload, ...prev];
26-
return next.length > MAX_ROWS ? next.slice(0, MAX_ROWS) : next;
27-
});
28-
}
29-
);
30-
return () => {
31-
unlisten.then((fn) => fn()).catch(() => {});
32-
};
33-
}, []);
17+
const rows = useMetaToolActivityStore((s) => s.rows);
3418

3519
return (
3620
<Card data-testid="meta-tool-audit-log">
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Global, navigation-persistent log of `mcpmux_*` meta-tool invocations.
3+
*
4+
* The audit panel (`MetaToolAuditLog`) previously kept rows in component-local
5+
* state with a `meta-tool-invoked` listener mounted only while the panel was
6+
* visible. That meant the list vanished on tab change and showed empty if you
7+
* navigated in *after* a call had already fired. This store lifts both the rows
8+
* and the listener to app scope: the listener starts once at launch and rows
9+
* persist for the whole UI session.
10+
*/
11+
12+
import { create } from 'zustand';
13+
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
14+
import type { MetaToolAuditEvent } from '@/lib/api/metaTools';
15+
16+
/** Ring-buffer size — most recent N invocations kept in memory. */
17+
export const MAX_META_TOOL_ROWS = 50;
18+
19+
interface MetaToolActivityState {
20+
rows: MetaToolAuditEvent[];
21+
push: (event: MetaToolAuditEvent) => void;
22+
clear: () => void;
23+
}
24+
25+
export const useMetaToolActivityStore = create<MetaToolActivityState>((set) => ({
26+
rows: [],
27+
push: (event) =>
28+
set((state) => {
29+
// Most-recent-first; trim to the ring-buffer size.
30+
const next = [event, ...state.rows];
31+
return { rows: next.length > MAX_META_TOOL_ROWS ? next.slice(0, MAX_META_TOOL_ROWS) : next };
32+
}),
33+
clear: () => set({ rows: [] }),
34+
}));
35+
36+
// Module-level singleton listener so it survives component unmounts (tab
37+
// changes) and is wired exactly once regardless of how many callers init it.
38+
let listening = false;
39+
let unlistenPromise: Promise<UnlistenFn> | null = null;
40+
41+
/**
42+
* Start the app-wide `meta-tool-invoked` listener (idempotent). Call once near
43+
* the app root so activity accumulates for the whole session, independent of
44+
* which tab is currently mounted.
45+
*/
46+
export function startMetaToolActivityListener(): void {
47+
if (listening) return;
48+
listening = true;
49+
unlistenPromise = listen<MetaToolAuditEvent>('meta-tool-invoked', (event) => {
50+
useMetaToolActivityStore.getState().push(event.payload);
51+
});
52+
}
53+
54+
/** Tear down the listener (mainly for tests / hot-reload hygiene). */
55+
export function stopMetaToolActivityListener(): void {
56+
listening = false;
57+
void unlistenPromise?.then((fn) => fn()).catch(() => {});
58+
unlistenPromise = null;
59+
}

0 commit comments

Comments
 (0)