Skip to content

Commit a215012

Browse files
authored
feat: @mux UX + Windows updater fix + minimal-first optimization (#171)
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent b90b05c commit a215012

8 files changed

Lines changed: 119 additions & 15 deletions

File tree

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
],
2727
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdCQUZGMEVCMEZBOTk5RTcKUldUbm1ha1A2L0N2ZTlOZjN5T3pGOHBHRUlibytMY2tPeWJkQ01heDJzdTJqK3B3a2lBdDZ1T1oK",
2828
"windows": {
29-
"installMode": "quiet"
29+
"installMode": "passive"
3030
}
3131
}
3232
},
@@ -50,7 +50,14 @@
5050
"bundle": {
5151
"active": true,
5252
"createUpdaterArtifacts": true,
53-
"targets": "all",
53+
"targets": [
54+
"app",
55+
"dmg",
56+
"deb",
57+
"rpm",
58+
"appimage",
59+
"nsis"
60+
],
5461
"icon": [
5562
"icons/32x32.png",
5663
"icons/128x128.png",

apps/desktop/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ function AppContent() {
104104
// into the new version — so a restart picks up updates with no clicks.
105105
// Otherwise just surface the dismissible banner for a manual install.
106106
useEffect(() => {
107+
// Never auto-update under `pnpm dev`. A dev build would otherwise detect a
108+
// newer published release, install it over this build, and relaunch — so
109+
// your local changes would vanish before you could see them. Production
110+
// builds (import.meta.env.DEV === false) are unaffected.
111+
if (import.meta.env.DEV) return;
112+
107113
const checkForUpdates = async () => {
108114
try {
109115
const { checkForUpdate } = await import('@/lib/updates');

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>

crates/mcpmux-gateway/src/mcp/handler.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,10 @@ impl ServerHandler for McpMuxGatewayHandler {
368368
list_feature_sets) are safe to call freely once the user has opted \
369369
in; writes (manage_feature_set, bind_current_workspace) prompt the \
370370
user for approval. Most operations accept an optional `space_id` \
371-
(from mcpmux_list_spaces) to target a specific Space."
371+
(from mcpmux_list_spaces) to target a specific Space. When \
372+
optimizing, start minimal: search for the few tools the task needs, \
373+
compose a small set, then expand it later as needs arise rather than \
374+
dumping the whole catalog upfront."
372375
.to_string(),
373376
);
374377
info

crates/mcpmux-gateway/src/services/meta_tools/tools.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ impl MetaTool for ListAllToolsTool {
116116
fn description(&self) -> &'static str {
117117
"List every tool installed in a Space (default: the caller's resolved \
118118
Space; pass `space_id` to target another), without the current \
119-
FeatureSet filter applied. Use this to see what could be exposed \
120-
before composing a custom FeatureSet. Returns an array of \
119+
FeatureSet filter applied. Prefer `mcpmux_search_tools` unless you need \
120+
the full list — this dump is token-heavy. Returns an array of \
121121
{server_id, qualified_name, description, available}."
122122
}
123123

@@ -716,7 +716,8 @@ impl MetaTool for ManageFeatureSetTool {
716716
(needs `feature_set_id`). Tool names are the qualified names from \
717717
`mcpmux_list_all_tools`/`mcpmux_search_tools`. Built-in sets can't be \
718718
modified. Requires user approval. Route a workspace through a FeatureSet \
719-
with `mcpmux_bind_current_workspace`."
719+
with `mcpmux_bind_current_workspace`. Prefer a small initial set you \
720+
expand later over adding everything upfront."
720721
}
721722

722723
fn input_schema(&self) -> Value {

tests/ts/components/App.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ describe('App – dynamic gateway URL display', () => {
288288

289289
describe('App – update banner', () => {
290290
beforeEach(() => {
291+
// The startup auto-update check is gated behind `!import.meta.env.DEV`
292+
// (it must never run under `pnpm dev`). Vitest runs in dev mode, so stub
293+
// DEV=false here to exercise the production update flow.
294+
vi.stubEnv('DEV', false);
291295
vi.useFakeTimers();
292296
gatewayEventCallbacks = [];
293297
setupInvoke({ get_version: '0.1.2' });
@@ -296,6 +300,7 @@ describe('App – update banner', () => {
296300

297301
afterEach(() => {
298302
vi.useRealTimers();
303+
vi.unstubAllEnvs();
299304
});
300305

301306
it('should show update banner when update is available', async () => {

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)