diff --git a/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx b/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx index c09d3334..e0c3b1c2 100644 --- a/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx +++ b/apps/desktop/src/features/featuresets/FeatureSetsPage.tsx @@ -12,6 +12,7 @@ import { AlertCircle, CheckCircle2, Zap, + Sparkles, } from 'lucide-react'; import { Card, @@ -268,6 +269,32 @@ export function FeatureSetsPage() { + {/* @mux self-optimization hint — let the assistant curate the toolset from chat */} +
+
+
+ +
+
+

+ Let your AI build these for you +

+

+ In any connected client, just say{' '} + + @mux optimize + {' '} + — your assistant can discover the available tools, compose a FeatureSet, and pin it + to the current folder. Every change is gated behind a one-click approval, so you + stay in control. +

+
+
+
+ {/* Feature-set model explainer */}
diff --git a/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx b/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx index ace428ae..99ce9cb3 100644 --- a/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx +++ b/apps/desktop/src/features/metaTools/MetaToolApprovalDialog.tsx @@ -3,6 +3,7 @@ import { listen } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api/core'; import { AlertTriangle, CheckCircle2, XCircle } from 'lucide-react'; import { Button, Card, CardContent, CardHeader, CardTitle } from '@mcpmux/ui'; +import { useNavigateTo } from '@/stores'; /** * Incoming approval request emitted by the gateway's ApprovalBroker. @@ -53,6 +54,7 @@ type Decision = 'allow_once' | 'always_for_this_session_and_client' | 'deny'; export function MetaToolApprovalDialog() { const [queue, setQueue] = useState([]); const current = queue[0]; + const navigateTo = useNavigateTo(); useEffect(() => { const unlistenPromise = listen( @@ -87,6 +89,16 @@ export function MetaToolApprovalDialog() { [current] ); + // "Prefer not to be asked?" escape hatch. Deny the current request first — + // fail-closed and immediate, so the calling client isn't left hanging for + // the full 60s broker timeout — then jump to the Built-in tab, where the + // "Require approval for tool changes" switch lets the user turn these + // prompts off entirely. + const manageApprovals = useCallback(() => { + void respond('deny'); + navigateTo('builtin-servers'); + }, [respond, navigateTo]); + // Normalize the freeform diff defensively — a missing field must never // throw (this previously crashed on `mcpmux_create_feature_set`, whose diff // is `{ added_tools }` and has no `after`). @@ -206,11 +218,22 @@ export function MetaToolApprovalDialog() {
- {queue.length > 1 && ( -

- {queue.length - 1} more pending… -

- )} +
+ + {queue.length > 1 && ( + + {queue.length - 1} more pending… + + )} +
diff --git a/tests/ts/components/MetaToolApprovalDialog.test.tsx b/tests/ts/components/MetaToolApprovalDialog.test.tsx index d0d1b6d2..e6a2d09d 100644 --- a/tests/ts/components/MetaToolApprovalDialog.test.tsx +++ b/tests/ts/components/MetaToolApprovalDialog.test.tsx @@ -1,16 +1,20 @@ /** * Approval dialog wiring: - * - renders the target-Space chip (so a cross-Space write is obvious), and + * - renders the target-Space chip (so a cross-Space write is obvious), * - survives a freeform `diff` shape (`{ added_tools }`) without crashing — * a regression guard for the earlier "Cannot read properties of undefined - * (reading 'length')" bug. + * (reading 'length')" bug, and + * - the "Manage approval prompts" link denies the request (fail-closed) and + * routes to the Built-in tab where prompts can be disabled. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -const { handlers } = vi.hoisted(() => ({ +const { handlers, navigateToSpy } = vi.hoisted(() => ({ handlers: new Map void>(), + navigateToSpy: vi.fn(), })); // Capture the dialog's `listen('meta-tool-approval-request', cb)` so the test @@ -22,7 +26,9 @@ vi.mock('@tauri-apps/api/event', () => ({ }), })); vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn().mockResolvedValue(undefined) })); +vi.mock('@/stores', () => ({ useNavigateTo: () => navigateToSpy })); +import { invoke } from '@tauri-apps/api/core'; import { MetaToolApprovalDialog } from '@/features/metaTools/MetaToolApprovalDialog'; async function emitRequest(payload: Record) { @@ -40,7 +46,11 @@ async function emitRequest(payload: Record) { } describe('MetaToolApprovalDialog', () => { - beforeEach(() => handlers.clear()); + beforeEach(() => { + handlers.clear(); + navigateToSpy.mockClear(); + vi.mocked(invoke).mockClear(); + }); it('names the target Space when present', async () => { render(); @@ -82,4 +92,26 @@ describe('MetaToolApprovalDialog', () => { expect(screen.getByText(/github_create_issue/)).toBeInTheDocument(); expect(screen.getByText(/slack_send/)).toBeInTheDocument(); }); + + it('"Manage approval prompts" denies the request and routes to the Built-in tab', async () => { + const user = userEvent.setup(); + render(); + await emitRequest({ + tool_name: 'mcpmux_bind_current_workspace', + summary: 'Bind this folder', + diff: null, + raw_args: {}, + affects_other_clients: false, + }); + + await user.click(await screen.findByTestId('meta-tool-approval-manage-link')); + + // Fail-closed: the pending request is denied rather than left to time out. + expect(invoke).toHaveBeenCalledWith( + 'respond_to_meta_tool_approval', + expect.objectContaining({ decision: 'deny' }) + ); + // ...and the user lands on the tab that hosts the approval toggle. + expect(navigateToSpy).toHaveBeenCalledWith('builtin-servers'); + }); });