Skip to content

Commit 8049796

Browse files
authored
Merge branch 'main' into fix/dependabot-security-overrides
2 parents d047b8d + d5df002 commit 8049796

28 files changed

Lines changed: 996 additions & 178 deletions

apps/desktop/src-tauri/src/commands/settings.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,48 @@ pub async fn set_update_channel(
203203
Ok(normalized.to_string())
204204
}
205205

206+
/// App-settings key for the "ask to map new folders" prompt switch.
207+
const WORKSPACE_MAPPING_PROMPT_KEY: &str = "workspaces.mapping_prompt_enabled";
208+
209+
/// Interpret a stored value for the workspace mapping-prompt toggle. Missing or
210+
/// any non-`"false"` value means **enabled** — the prompt is on by default, so
211+
/// only an explicit opt-out turns it off.
212+
fn mapping_prompt_enabled_from(stored: Option<&str>) -> bool {
213+
stored.map(|v| v != "false").unwrap_or(true)
214+
}
215+
216+
/// Whether McpMux pops the "map this folder?" sheet when a connected client
217+
/// opens a folder that has no explicit binding (it's on the default Starter
218+
/// set). Default **true**. Users who find the prompt noisy can turn it off
219+
/// here or via the link in the sheet itself.
220+
#[tauri::command]
221+
pub async fn get_workspace_mapping_prompt_enabled(
222+
app_state: State<'_, AppState>,
223+
) -> Result<bool, String> {
224+
let stored = app_state
225+
.settings_repository
226+
.get(WORKSPACE_MAPPING_PROMPT_KEY)
227+
.await
228+
.map_err(|e| e.to_string())?;
229+
Ok(mapping_prompt_enabled_from(stored.as_deref()))
230+
}
231+
232+
/// Enable/disable the "map this folder?" prompt. Persisted; returns the value
233+
/// actually saved.
234+
#[tauri::command]
235+
pub async fn set_workspace_mapping_prompt_enabled(
236+
enabled: bool,
237+
app_state: State<'_, AppState>,
238+
) -> Result<bool, String> {
239+
app_state
240+
.settings_repository
241+
.set(WORKSPACE_MAPPING_PROMPT_KEY, &enabled.to_string())
242+
.await
243+
.map_err(|e| e.to_string())?;
244+
info!("[Settings] Workspace mapping prompt set to {}", enabled);
245+
Ok(enabled)
246+
}
247+
206248
/// Check if app should start hidden (for auto-launch with --hidden flag)
207249
pub fn should_start_hidden() -> bool {
208250
let args: Vec<String> = std::env::args().collect();
@@ -332,4 +374,16 @@ mod tests {
332374
assert!(out == UPDATE_CHANNEL_STABLE || out == UPDATE_CHANNEL_PRERELEASE);
333375
}
334376
}
377+
378+
#[test]
379+
fn test_mapping_prompt_enabled_defaults_on() {
380+
// Missing setting → on by default.
381+
assert!(mapping_prompt_enabled_from(None));
382+
// Only an explicit "false" disables it.
383+
assert!(!mapping_prompt_enabled_from(Some("false")));
384+
assert!(mapping_prompt_enabled_from(Some("true")));
385+
// Any unexpected value is treated as enabled (fail-open to the default).
386+
assert!(mapping_prompt_enabled_from(Some("")));
387+
assert!(mapping_prompt_enabled_from(Some("garbage")));
388+
}
335389
}

apps/desktop/src-tauri/src/commands/workspace_binding.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -445,11 +445,11 @@ pub struct WorkspaceEffectiveFeaturesDto {
445445
/// trailing slash, etc.).
446446
pub workspace_root: String,
447447
/// `binding` when a `WorkspaceBinding` matched the longest prefix of
448-
/// the root; `unbound` when no binding matched. With the new resolver,
449-
/// `unbound` means a live roots-capable session for this folder would
450-
/// be **denied** — the `feature_sets` field below shows the default
451-
/// Space's Default FS purely as a *preview* of what binding the folder
452-
/// to that FS would expose, not as the active routing target.
448+
/// the root; `unbound` when no binding matched. An `unbound` folder is
449+
/// **not** denied — it falls back to the default Space's Starter FS, so
450+
/// the `feature_sets` field below is exactly what a live session for this
451+
/// folder sees right now (the active routing target), until the user
452+
/// attaches an explicit binding to override it.
453453
pub source: String,
454454
/// `Some(id)` only when `source == "binding"`.
455455
pub binding_id: Option<String>,
@@ -588,11 +588,11 @@ pub async fn get_workspace_effective_features(
588588
b.feature_set_ids,
589589
),
590590
None => {
591-
// Source = `unbound` mirrors the new resolver: a live session
592-
// here would be denied. We still surface the default Space's
593-
// Default FS as a *preview* so the UI can render "if you bound
594-
// this folder to <FS>, here's what it would see" — it's
595-
// informational, not the active routing target.
591+
// Source = `unbound` mirrors the resolver: an unmapped folder
592+
// falls back to the default Space's Starter FS. This is the
593+
// active routing target a live session here resolves to, not a
594+
// hypothetical preview — the user can attach a binding to give
595+
// the folder something other than the default.
596596
let starter_fs = state
597597
.feature_set_repository
598598
.get_starter_for_space(&default_space.id.to_string())

apps/desktop/src-tauri/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,8 @@ pub fn run() {
992992
commands::set_auto_install_updates,
993993
commands::get_update_channel,
994994
commands::set_update_channel,
995+
commands::get_workspace_mapping_prompt_enabled,
996+
commands::set_workspace_mapping_prompt_enabled,
995997
])
996998
.build(tauri::generate_context!())
997999
.expect("error while building McpMux application")

apps/desktop/src/features/clients/ClientsPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -873,9 +873,9 @@ function RootlessGrantsSection({
873873
<div className="mt-3 flex items-start gap-2 rounded-lg border border-[rgb(var(--border-subtle))] bg-[rgb(var(--surface))] p-2.5">
874874
<ShieldOff className="mt-0.5 h-4 w-4 flex-shrink-0 text-[rgb(var(--muted))]" />
875875
<p className="text-[11px] text-[rgb(var(--muted))]">
876-
No defaults set — rootless sessions from this client are denied. That&apos;s the safe
877-
default. Pick a FeatureSet above only if you trust this client to operate without a
878-
workspace folder.
876+
No per-client defaults set — rootless sessions from this client fall back to your
877+
default Starter set. Pick a FeatureSet above to grant this client a specific set
878+
instead.
879879
</p>
880880
</div>
881881
)}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ export function FeatureSetPanel({ featureSet, spaceId, onClose, onDelete, onUpda
5858

5959
// Both FS types are member-driven now.
6060
const isConfigurable = true;
61-
// The auto-seeded "Starter" FS is treated identically to a Custom one
62-
// — the type tag is a UI hint, not a routing flag.
61+
// The auto-seeded "Starter" FS has editable membership like a Custom one
62+
// (change which tools it includes, or empty it). What's locked is its
63+
// identity + lifecycle: it's the default fallback for unmapped folders, so
64+
// its name is fixed (the backend ignores name changes on builtin rows) and
65+
// it can't be deleted — the Delete action below is gated to Custom sets.
6366
const isStarter = isStarterFeatureSet(featureSet);
6467
const isCustom = featureSet.feature_set_type === 'custom';
6568

@@ -257,7 +260,7 @@ export function FeatureSetPanel({ featureSet, spaceId, onClose, onDelete, onUpda
257260
<span
258261
title={
259262
isStarter
260-
? 'Auto-created with this Space. Edit, rename, or delete freely — no special routing role.'
263+
? "Auto-created with this Space. The default set for folders you haven't mapped — edit which tools it includes; its name is fixed and it can't be deleted."
261264
: undefined
262265
}
263266
className={`text-[10px] px-1.5 py-0.5 rounded-full font-medium border ${
@@ -339,7 +342,7 @@ export function FeatureSetPanel({ featureSet, spaceId, onClose, onDelete, onUpda
339342
<div className="flex gap-2">
340343
<Star className="h-4 w-4 text-yellow-500 flex-shrink-0 mt-0.5" />
341344
<div className="text-xs text-yellow-800 dark:text-yellow-200">
342-
<strong>Starter FeatureSet:</strong> auto-created with this Space. It&apos;s an ordinary FeatureSet — edit, rename, or delete it freely. <em>No special routing role:</em> Workspace bindings and per-client grants pick FeatureSets explicitly.
345+
<strong>Starter FeatureSet:</strong> auto-created with this Space and used as the <em>default</em> for folders you haven&apos;t explicitly mapped (and rootless sessions). Edit which tools it includes (or empty it) to change what they get. Its name is fixed and it <strong>can&apos;t be deleted</strong>, since the fallback always needs a stable target.
343346
</div>
344347
</div>
345348
</div>

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,9 @@ export function FeatureSetsPage() {
199199
})
200200
.sort((a, b) => {
201201
// Starter FS first (pinned to top — operator usually wants the
202-
// auto-seeded one near the top so they can edit / delete it
203-
// first), then Custom sets alphabetically. The 'default' key is
204-
// kept so a stale row read pre-migration still sorts correctly.
202+
// auto-seeded default set near the top so they can edit it first),
203+
// then Custom sets alphabetically. The 'default' key is kept so a
204+
// stale row read pre-migration still sorts correctly.
205205
const order: Record<string, number> = {
206206
starter: 0,
207207
default: 0,
@@ -375,7 +375,7 @@ export function FeatureSetsPage() {
375375
{isStarter && (
376376
<div
377377
className="absolute right-3 top-3 flex items-center gap-1.5 rounded-full bg-gradient-to-r from-emerald-500 to-green-500 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-white shadow-[0_4px_12px_-2px_rgb(16_185_129/0.5)]"
378-
title="Auto-seeded with this Space. Edit, rename, or delete freely — no special routing role; bindings and per-client grants pick FeatureSets explicitly."
378+
title="Auto-seeded with this Space. The default set for folders you haven't mapped — edit which tools it includes to change what they get. Its name is fixed and it can't be deleted."
379379
data-testid={`featureset-starter-badge-${fs.id}`}
380380
>
381381
<CheckCircle2 className="h-3 w-3" />

apps/desktop/src/features/settings/SettingsPage.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export function SettingsPage() {
7272
const [logRetentionDays, setLogRetentionDays] = useState<number>(30);
7373
const [savingRetention, setSavingRetention] = useState(false);
7474

75+
// Workspace mapping prompt — pops the "map this folder?" sheet when a client
76+
// opens an unmapped folder. On by default.
77+
const [mappingPromptEnabled, setMappingPromptEnabled] = useState(true);
78+
const [savingMappingPrompt, setSavingMappingPrompt] = useState(false);
79+
7580
// Meta-tools master switch — gates the entire `mcpmux_*` namespace.
7681

7782
// Gateway port — persisted user override, the default the app ships
@@ -212,6 +217,34 @@ export function SettingsPage() {
212217
loadStartupSettings();
213218
}, []);
214219

220+
// Load workspace mapping-prompt setting on mount.
221+
useEffect(() => {
222+
invoke<boolean>('get_workspace_mapping_prompt_enabled')
223+
.then(setMappingPromptEnabled)
224+
.catch((err) => console.error('Failed to load mapping prompt setting:', err));
225+
}, []);
226+
227+
const updateMappingPrompt = async (enabled: boolean) => {
228+
const prev = mappingPromptEnabled;
229+
setMappingPromptEnabled(enabled);
230+
setSavingMappingPrompt(true);
231+
try {
232+
await invoke('set_workspace_mapping_prompt_enabled', { enabled });
233+
success(
234+
'Settings saved',
235+
enabled
236+
? "You'll be asked to map new folders."
237+
: 'New-folder prompts are off — unmapped folders still use your default Starter set.'
238+
);
239+
} catch (err) {
240+
const msg = err instanceof Error ? err.message : 'Unknown error';
241+
error('Failed to save setting', msg);
242+
setMappingPromptEnabled(prev);
243+
} finally {
244+
setSavingMappingPrompt(false);
245+
}
246+
};
247+
215248
// Save startup settings when they change
216249
const updateStartupSetting = async (key: keyof StartupSettings, value: boolean) => {
217250
console.log(`[Settings] Updating ${key} to ${value}`);
@@ -524,6 +557,40 @@ export function SettingsPage() {
524557
</CardContent>
525558
</Card>
526559

560+
{/* Workspaces Section */}
561+
<Card data-testid="settings-workspaces-section">
562+
<CardHeader>
563+
<CardTitle className="flex items-center gap-2">
564+
<FolderOpen className="h-5 w-5" />
565+
Workspaces
566+
</CardTitle>
567+
<CardDescription>
568+
How McpMux handles folders your connected apps open.
569+
</CardDescription>
570+
</CardHeader>
571+
<CardContent>
572+
<div className="flex items-center justify-between gap-4">
573+
<div className="flex min-w-0 flex-1 items-start gap-3">
574+
<FolderOpen className="mt-0.5 h-5 w-5 flex-shrink-0 text-[rgb(var(--muted))]" />
575+
<div>
576+
<label className="text-sm font-medium">Ask to map new folders</label>
577+
<p className="mt-1 text-xs text-[rgb(var(--muted))]">
578+
When a connected app opens a folder you haven't mapped, show a prompt to give
579+
it a specific feature set. The folder already works with your default Starter
580+
set either way.
581+
</p>
582+
</div>
583+
</div>
584+
<Switch
585+
checked={mappingPromptEnabled}
586+
onCheckedChange={updateMappingPrompt}
587+
disabled={savingMappingPrompt}
588+
data-testid="workspace-mapping-prompt-switch"
589+
/>
590+
</div>
591+
</CardContent>
592+
</Card>
593+
527594
{/* Appearance Section */}
528595
<Card>
529596
<CardHeader>

0 commit comments

Comments
 (0)