Skip to content

Commit f9dc7d6

Browse files
committed
feat(ui): surface @mux tool optimization + approval opt-out link
Two small discoverability nudges around tool optimization: - FeatureSets tab: add a branded "Let your AI build these for you" hint showing the `@mux optimize` chat affordance — the assistant can discover tools, compose a FeatureSet, and pin it to the current folder, each gated behind a one-click approval. - Meta-tool approval dialog: add a "Prefer not to be asked? Manage approval prompts" link that denies the current request (fail-closed, so the client is not left hanging for the 60s broker timeout) and routes to the Built-in tab, where the "Require approval for tool changes" switch can turn these prompts off. Covered by a new MetaToolApprovalDialog test asserting the link denies and navigates to builtin-servers. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 92f8ac2 commit f9dc7d6

3 files changed

Lines changed: 91 additions & 9 deletions

File tree

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
AlertCircle,
1313
CheckCircle2,
1414
Zap,
15+
Sparkles,
1516
} from 'lucide-react';
1617
import {
1718
Card,
@@ -268,6 +269,32 @@ export function FeatureSetsPage() {
268269
</div>
269270
</div>
270271

272+
{/* @mux self-optimization hint — let the assistant curate the toolset from chat */}
273+
<div className="flex-shrink-0 px-8 pt-6">
274+
<div
275+
className="mx-auto flex max-w-[2000px] items-start gap-3 rounded-xl border border-violet-200/70 bg-gradient-to-r from-violet-50/60 to-transparent p-4 dark:border-violet-800/40 dark:from-violet-900/15"
276+
data-testid="featuresets-mux-hint"
277+
>
278+
<div className="mt-0.5 flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-fuchsia-500 text-white shadow-[0_4px_10px_-2px_rgb(139_92_246/0.45)]">
279+
<Sparkles className="h-4 w-4 fill-current" />
280+
</div>
281+
<div className="min-w-0 flex-1">
282+
<p className="text-sm font-semibold text-[rgb(var(--foreground))]">
283+
Let your AI build these for you
284+
</p>
285+
<p className="mt-0.5 text-xs leading-relaxed text-[rgb(var(--muted))]">
286+
In any connected client, just say{' '}
287+
<code className="rounded bg-[rgb(var(--surface))] px-1.5 py-0.5 font-mono text-[11px] text-violet-600 dark:text-violet-300">
288+
@mux optimize
289+
</code>{' '}
290+
— your assistant can discover the available tools, compose a FeatureSet, and pin it
291+
to the current folder. Every change is gated behind a one-click approval, so you
292+
stay in control.
293+
</p>
294+
</div>
295+
</div>
296+
</div>
297+
271298
{/* Feature-set model explainer */}
272299
<div className="flex-shrink-0 px-8 pt-6">
273300
<div className="mx-auto flex max-w-[2000px] items-start gap-3 rounded-xl border border-emerald-200/70 bg-gradient-to-r from-emerald-50/60 to-transparent p-4 dark:border-emerald-800/40 dark:from-emerald-900/15">

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { listen } from '@tauri-apps/api/event';
33
import { invoke } from '@tauri-apps/api/core';
44
import { AlertTriangle, CheckCircle2, XCircle } from 'lucide-react';
55
import { Button, Card, CardContent, CardHeader, CardTitle } from '@mcpmux/ui';
6+
import { useNavigateTo } from '@/stores';
67

78
/**
89
* Incoming approval request emitted by the gateway's ApprovalBroker.
@@ -53,6 +54,7 @@ type Decision = 'allow_once' | 'always_for_this_session_and_client' | 'deny';
5354
export function MetaToolApprovalDialog() {
5455
const [queue, setQueue] = useState<ApprovalRequest[]>([]);
5556
const current = queue[0];
57+
const navigateTo = useNavigateTo();
5658

5759
useEffect(() => {
5860
const unlistenPromise = listen<ApprovalRequest>(
@@ -87,6 +89,16 @@ export function MetaToolApprovalDialog() {
8789
[current]
8890
);
8991

92+
// "Prefer not to be asked?" escape hatch. Deny the current request first —
93+
// fail-closed and immediate, so the calling client isn't left hanging for
94+
// the full 60s broker timeout — then jump to the Built-in tab, where the
95+
// "Require approval for tool changes" switch lets the user turn these
96+
// prompts off entirely.
97+
const manageApprovals = useCallback(() => {
98+
void respond('deny');
99+
navigateTo('builtin-servers');
100+
}, [respond, navigateTo]);
101+
90102
// Normalize the freeform diff defensively — a missing field must never
91103
// throw (this previously crashed on `mcpmux_create_feature_set`, whose diff
92104
// is `{ added_tools }` and has no `after`).
@@ -206,11 +218,22 @@ export function MetaToolApprovalDialog() {
206218
</Button>
207219
</div>
208220

209-
{queue.length > 1 && (
210-
<p className="text-[11px] text-[rgb(var(--muted))] text-right pt-1">
211-
{queue.length - 1} more pending…
212-
</p>
213-
)}
221+
<div className="flex items-center justify-between gap-3 pt-1">
222+
<button
223+
type="button"
224+
onClick={manageApprovals}
225+
className="text-[11px] text-[rgb(var(--muted))] underline-offset-2 hover:text-[rgb(var(--foreground))] hover:underline"
226+
title="Deny this request and open the Built-in tab, where you can turn off approval prompts for tool changes"
227+
data-testid="meta-tool-approval-manage-link"
228+
>
229+
Prefer not to be asked? Manage approval prompts →
230+
</button>
231+
{queue.length > 1 && (
232+
<span className="text-[11px] text-[rgb(var(--muted))]">
233+
{queue.length - 1} more pending…
234+
</span>
235+
)}
236+
</div>
214237
</CardContent>
215238
</Card>
216239
</div>

tests/ts/components/MetaToolApprovalDialog.test.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
/**
22
* Approval dialog wiring:
3-
* - renders the target-Space chip (so a cross-Space write is obvious), and
3+
* - renders the target-Space chip (so a cross-Space write is obvious),
44
* - survives a freeform `diff` shape (`{ added_tools }`) without crashing —
55
* a regression guard for the earlier "Cannot read properties of undefined
6-
* (reading 'length')" bug.
6+
* (reading 'length')" bug, and
7+
* - the "Manage approval prompts" link denies the request (fail-closed) and
8+
* routes to the Built-in tab where prompts can be disabled.
79
*/
810

911
import { describe, it, expect, vi, beforeEach } from 'vitest';
1012
import { render, screen, act } from '@testing-library/react';
13+
import userEvent from '@testing-library/user-event';
1114

12-
const { handlers } = vi.hoisted(() => ({
15+
const { handlers, navigateToSpy } = vi.hoisted(() => ({
1316
handlers: new Map<string, (e: { payload: unknown }) => void>(),
17+
navigateToSpy: vi.fn(),
1418
}));
1519

1620
// Capture the dialog's `listen('meta-tool-approval-request', cb)` so the test
@@ -22,7 +26,9 @@ vi.mock('@tauri-apps/api/event', () => ({
2226
}),
2327
}));
2428
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn().mockResolvedValue(undefined) }));
29+
vi.mock('@/stores', () => ({ useNavigateTo: () => navigateToSpy }));
2530

31+
import { invoke } from '@tauri-apps/api/core';
2632
import { MetaToolApprovalDialog } from '@/features/metaTools/MetaToolApprovalDialog';
2733

2834
async function emitRequest(payload: Record<string, unknown>) {
@@ -40,7 +46,11 @@ async function emitRequest(payload: Record<string, unknown>) {
4046
}
4147

4248
describe('MetaToolApprovalDialog', () => {
43-
beforeEach(() => handlers.clear());
49+
beforeEach(() => {
50+
handlers.clear();
51+
navigateToSpy.mockClear();
52+
vi.mocked(invoke).mockClear();
53+
});
4454

4555
it('names the target Space when present', async () => {
4656
render(<MetaToolApprovalDialog />);
@@ -82,4 +92,26 @@ describe('MetaToolApprovalDialog', () => {
8292
expect(screen.getByText(/github_create_issue/)).toBeInTheDocument();
8393
expect(screen.getByText(/slack_send/)).toBeInTheDocument();
8494
});
95+
96+
it('"Manage approval prompts" denies the request and routes to the Built-in tab', async () => {
97+
const user = userEvent.setup();
98+
render(<MetaToolApprovalDialog />);
99+
await emitRequest({
100+
tool_name: 'mcpmux_bind_current_workspace',
101+
summary: 'Bind this folder',
102+
diff: null,
103+
raw_args: {},
104+
affects_other_clients: false,
105+
});
106+
107+
await user.click(await screen.findByTestId('meta-tool-approval-manage-link'));
108+
109+
// Fail-closed: the pending request is denied rather than left to time out.
110+
expect(invoke).toHaveBeenCalledWith(
111+
'respond_to_meta_tool_approval',
112+
expect.objectContaining({ decision: 'deny' })
113+
);
114+
// ...and the user lands on the tab that hosts the approval toggle.
115+
expect(navigateToSpy).toHaveBeenCalledWith('builtin-servers');
116+
});
85117
});

0 commit comments

Comments
 (0)