diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index b93e1e59..3caf0961 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -26,7 +26,7 @@ ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDdCQUZGMEVCMEZBOTk5RTcKUldUbm1ha1A2L0N2ZTlOZjN5T3pGOHBHRUlibytMY2tPeWJkQ01heDJzdTJqK3B3a2lBdDZ1T1oK", "windows": { - "installMode": "quiet" + "installMode": "passive" } } }, @@ -50,7 +50,14 @@ "bundle": { "active": true, "createUpdaterArtifacts": true, - "targets": "all", + "targets": [ + "app", + "dmg", + "deb", + "rpm", + "appimage", + "nsis" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 3741edb4..b0d64656 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -104,6 +104,12 @@ function AppContent() { // into the new version — so a restart picks up updates with no clicks. // Otherwise just surface the dismissible banner for a manual install. useEffect(() => { + // Never auto-update under `pnpm dev`. A dev build would otherwise detect a + // newer published release, install it over this build, and relaunch — so + // your local changes would vanish before you could see them. Production + // builds (import.meta.env.DEV === false) are unaffected. + if (import.meta.env.DEV) return; + const checkForUpdates = async () => { try { const { checkForUpdate } = await import('@/lib/updates'); 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/crates/mcpmux-gateway/src/mcp/handler.rs b/crates/mcpmux-gateway/src/mcp/handler.rs index 2a50d315..8f3b0843 100644 --- a/crates/mcpmux-gateway/src/mcp/handler.rs +++ b/crates/mcpmux-gateway/src/mcp/handler.rs @@ -368,7 +368,10 @@ impl ServerHandler for McpMuxGatewayHandler { list_feature_sets) are safe to call freely once the user has opted \ in; writes (manage_feature_set, bind_current_workspace) prompt the \ user for approval. Most operations accept an optional `space_id` \ - (from mcpmux_list_spaces) to target a specific Space." + (from mcpmux_list_spaces) to target a specific Space. When \ + optimizing, start minimal: search for the few tools the task needs, \ + compose a small set, then expand it later as needs arise rather than \ + dumping the whole catalog upfront." .to_string(), ); info diff --git a/crates/mcpmux-gateway/src/services/meta_tools/tools.rs b/crates/mcpmux-gateway/src/services/meta_tools/tools.rs index 0675c32f..acedad0f 100644 --- a/crates/mcpmux-gateway/src/services/meta_tools/tools.rs +++ b/crates/mcpmux-gateway/src/services/meta_tools/tools.rs @@ -116,8 +116,8 @@ impl MetaTool for ListAllToolsTool { fn description(&self) -> &'static str { "List every tool installed in a Space (default: the caller's resolved \ Space; pass `space_id` to target another), without the current \ - FeatureSet filter applied. Use this to see what could be exposed \ - before composing a custom FeatureSet. Returns an array of \ + FeatureSet filter applied. Prefer `mcpmux_search_tools` unless you need \ + the full list — this dump is token-heavy. Returns an array of \ {server_id, qualified_name, description, available}." } @@ -716,7 +716,8 @@ impl MetaTool for ManageFeatureSetTool { (needs `feature_set_id`). Tool names are the qualified names from \ `mcpmux_list_all_tools`/`mcpmux_search_tools`. Built-in sets can't be \ modified. Requires user approval. Route a workspace through a FeatureSet \ - with `mcpmux_bind_current_workspace`." + with `mcpmux_bind_current_workspace`. Prefer a small initial set you \ + expand later over adding everything upfront." } fn input_schema(&self) -> Value { diff --git a/tests/ts/components/App.test.tsx b/tests/ts/components/App.test.tsx index d653ce5c..0fd35105 100644 --- a/tests/ts/components/App.test.tsx +++ b/tests/ts/components/App.test.tsx @@ -288,6 +288,10 @@ describe('App – dynamic gateway URL display', () => { describe('App – update banner', () => { beforeEach(() => { + // The startup auto-update check is gated behind `!import.meta.env.DEV` + // (it must never run under `pnpm dev`). Vitest runs in dev mode, so stub + // DEV=false here to exercise the production update flow. + vi.stubEnv('DEV', false); vi.useFakeTimers(); gatewayEventCallbacks = []; setupInvoke({ get_version: '0.1.2' }); @@ -296,6 +300,7 @@ describe('App – update banner', () => { afterEach(() => { vi.useRealTimers(); + vi.unstubAllEnvs(); }); it('should show update banner when update is available', async () => { 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'); + }); });