Skip to content

Commit ffbf14a

Browse files
committed
Introduce ModalDialog
- Now all soft modals use the same dialog component - All are draggable - Saved some code on the duplicated event handlers - Ensured MCP server knows about _all_ the dialogs we have (safety added with TypeScript) - Fixed a closing bug in one of the more obscure dialogs
1 parent cf3f41f commit ffbf14a

22 files changed

Lines changed: 622 additions & 666 deletions

apps/desktop/coverage-allowlist.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"$comment": "Files listed here are exempt from coverage thresholds. Each entry should include a reason. Remove entries as you add tests.",
33
"files": {
44
"ui/AlertDialog.svelte": { "reason": "Simple UI modal for informational messages" },
5-
"ui/DraggableDialog.svelte": { "reason": "Shared draggable dialog wrapper, depends on DOM APIs" },
5+
"ui/dialog-registry.ts": { "reason": "Pure constant and type definition, no logic to test" },
6+
"ui/ModalDialog.svelte": { "reason": "Shared modal dialog wrapper, depends on DOM APIs and Tauri commands" },
67
"updates/UpdateNotification.svelte": { "reason": "UI component, needs component testing" },
78
"app-status-store.ts": { "reason": "Depends on Tauri APIs" },
89
"benchmark.ts": { "reason": "Dev tooling, not critical path" },

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ pub fn run() {
368368
mcp::pane_state::update_focused_pane,
369369
mcp::dialog_state::notify_dialog_opened,
370370
mcp::dialog_state::notify_dialog_closed,
371+
mcp::dialog_state::register_known_dialogs,
371372
mcp::settings_state::mcp_update_settings_state,
372373
mcp::settings_state::mcp_update_settings_open,
373374
mcp::settings_state::mcp_update_settings_section,

apps/desktop/src-tauri/src/mcp/dialog_state.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
//! Soft dialog tracking for MCP context tools.
22
//!
3-
//! Tracks in-page overlay dialogs (about, license, copy-confirmation, mkdir-confirmation).
3+
//! Tracks in-page overlay dialogs (about, license, copy-confirmation, etc.).
44
//! Window-based dialogs (settings, file viewers) are derived from Tauri's window manager
55
//! in resources.rs — no manual tracking needed for those.
6+
//!
7+
//! The frontend registers all known soft dialog IDs at startup via
8+
//! `register_known_dialogs`, so the MCP "available dialogs" resource
9+
//! stays in sync with the actual Svelte components automatically.
610
711
use std::collections::HashSet;
812
use std::sync::RwLock;
13+
use serde::Deserialize;
914
use tauri::{AppHandle, Manager};
1015

11-
/// Tracks which soft (overlay) dialogs are currently open.
12-
/// Uses a simple set of dialog type strings.
16+
/// A dialog type registered by the frontend at startup.
17+
#[derive(Debug, Clone, Deserialize)]
18+
pub struct KnownDialog {
19+
pub id: String,
20+
pub description: Option<String>,
21+
}
22+
23+
/// Tracks which soft (overlay) dialogs are currently open,
24+
/// and which dialog types are known (registered at startup).
1325
#[derive(Debug, Default)]
1426
pub struct SoftDialogTracker {
1527
open: RwLock<HashSet<String>>,
28+
known: RwLock<Vec<KnownDialog>>,
1629
}
1730

1831
impl SoftDialogTracker {
1932
pub fn new() -> Self {
2033
Self {
2134
open: RwLock::new(HashSet::new()),
35+
known: RwLock::new(Vec::new()),
2236
}
2337
}
2438

@@ -33,6 +47,14 @@ impl SoftDialogTracker {
3347
pub fn get_open_types(&self) -> Vec<String> {
3448
self.open.read().unwrap().iter().cloned().collect()
3549
}
50+
51+
pub fn register_known(&self, dialogs: Vec<KnownDialog>) {
52+
*self.known.write().unwrap() = dialogs;
53+
}
54+
55+
pub fn get_known_dialogs(&self) -> Vec<KnownDialog> {
56+
self.known.read().unwrap().clone()
57+
}
3658
}
3759

3860
/// Tauri command: frontend notifies that a soft dialog opened.
@@ -51,6 +73,14 @@ pub fn notify_dialog_closed(app: AppHandle, dialog_type: String) {
5173
}
5274
}
5375

76+
/// Tauri command: frontend registers all known soft dialog types at startup.
77+
#[tauri::command]
78+
pub fn register_known_dialogs(app: AppHandle, dialogs: Vec<KnownDialog>) {
79+
if let Some(tracker) = app.try_state::<SoftDialogTracker>() {
80+
tracker.register_known(dialogs);
81+
}
82+
}
83+
5484
#[cfg(test)]
5585
mod tests {
5686
use super::*;
@@ -87,4 +117,41 @@ mod tests {
87117
tracker.close("nonexistent"); // Should not panic
88118
assert!(tracker.get_open_types().is_empty());
89119
}
120+
121+
#[test]
122+
fn test_register_known_dialogs() {
123+
let tracker = SoftDialogTracker::new();
124+
assert!(tracker.get_known_dialogs().is_empty());
125+
126+
let dialogs = vec![
127+
KnownDialog { id: "about".to_string(), description: None },
128+
KnownDialog { id: "alert".to_string(), description: None },
129+
KnownDialog {
130+
id: "copy-confirmation".to_string(),
131+
description: Some("Opened by the copy tool".to_string()),
132+
},
133+
];
134+
tracker.register_known(dialogs);
135+
136+
let known = tracker.get_known_dialogs();
137+
assert_eq!(known.len(), 3);
138+
assert_eq!(known[0].id, "about");
139+
assert_eq!(known[2].description.as_deref(), Some("Opened by the copy tool"));
140+
}
141+
142+
#[test]
143+
fn test_register_known_replaces_previous() {
144+
let tracker = SoftDialogTracker::new();
145+
146+
tracker.register_known(vec![
147+
KnownDialog { id: "about".to_string(), description: None },
148+
]);
149+
assert_eq!(tracker.get_known_dialogs().len(), 1);
150+
151+
tracker.register_known(vec![
152+
KnownDialog { id: "about".to_string(), description: None },
153+
KnownDialog { id: "alert".to_string(), description: None },
154+
]);
155+
assert_eq!(tracker.get_known_dialogs().len(), 2);
156+
}
90157
}

apps/desktop/src-tauri/src/mcp/resources.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,31 @@ fn extract_viewer_path<R: Runtime>(window: &WebviewWindow<R>) -> Option<String>
181181
.map(|(_, value)| value.into_owned())
182182
}
183183

184+
/// Build YAML for the "available dialogs" resource.
185+
/// Combines window-based types (hardcoded, stable) with soft dialog types
186+
/// registered by the frontend at startup.
187+
fn build_available_dialogs_yaml<R: Runtime>(app: &tauri::AppHandle<R>) -> String {
188+
let mut yaml = String::new();
189+
190+
// Window-based dialog types (managed on the Rust side)
191+
yaml.push_str("- type: settings\n sections: [general, appearance, shortcuts, advanced]\n");
192+
yaml.push_str(
193+
"- type: file-viewer\n description: Opens for file under cursor, or specify path. Multiple can be open.\n",
194+
);
195+
196+
// Soft dialog types (registered by the frontend)
197+
if let Some(tracker) = app.try_state::<SoftDialogTracker>() {
198+
for dialog in tracker.get_known_dialogs() {
199+
yaml.push_str(&format!("- type: {}\n", dialog.id));
200+
if let Some(ref desc) = dialog.description {
201+
yaml.push_str(&format!(" description: {}\n", desc));
202+
}
203+
}
204+
}
205+
206+
yaml
207+
}
208+
184209
/// Read a resource by URI.
185210
pub fn read_resource<R: Runtime>(app: &tauri::AppHandle<R>, uri: &str) -> Result<ResourceContent, String> {
186211
let (content, mime_type) = match uri {
@@ -260,17 +285,8 @@ pub fn read_resource<R: Runtime>(app: &tauri::AppHandle<R>, uri: &str) -> Result
260285
(yaml, "text/yaml")
261286
}
262287
"cmdr://dialogs/available" => {
263-
let yaml = r#"- type: settings
264-
sections: [general, appearance, shortcuts, advanced]
265-
- type: file-viewer
266-
description: Opens for file under cursor, or specify path. Multiple can be open.
267-
- type: about
268-
- type: copy-confirmation
269-
description: Opened by the copy tool, not directly. Can only be closed.
270-
- type: mkdir-confirmation
271-
description: Opened by the mkdir tool, not directly. Can only be closed.
272-
"#;
273-
(yaml.to_string(), "text/yaml")
288+
let yaml = build_available_dialogs_yaml(app);
289+
(yaml, "text/yaml")
274290
}
275291
_ => return Err(format!("Unknown resource URI: {}", uri)),
276292
};

apps/desktop/src/app.css

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
--color-error: #d32f2f;
4646
--color-error-bg: #fef2f2;
4747
--color-error-border: #fecaca;
48-
--color-error-text: #b91c1c;
4948
--color-warning: #e65100;
5049
--color-warning-bg: rgba(230, 81, 0, 0.1);
5150

@@ -107,7 +106,6 @@
107106
--color-error: #f44336;
108107
--color-error-bg: #450a0a;
109108
--color-error-border: #7f1d1d;
110-
--color-error-text: #fca5a5;
111109
--color-warning: #f5a623;
112110
--color-warning-bg: rgba(245, 166, 35, 0.15);
113111

apps/desktop/src/lib/file-operations/copy/CopyDialog.svelte

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
onScanPreviewError,
1111
onScanPreviewCancelled,
1212
scanVolumeForConflicts,
13-
notifyDialogOpened,
14-
notifyDialogClosed,
1513
type VolumeSpaceInfo,
1614
type VolumeConflictInfo,
1715
type SourceItemInput,
@@ -20,7 +18,7 @@
2018
import type { VolumeInfo, SortColumn, SortOrder, ConflictResolution } from '$lib/file-explorer/types'
2119
import { getSetting } from '$lib/settings'
2220
import DirectionIndicator from './DirectionIndicator.svelte'
23-
import DraggableDialog from '$lib/ui/DraggableDialog.svelte'
21+
import ModalDialog from '$lib/ui/ModalDialog.svelte'
2422
import { generateTitle } from './copy-dialog-utils'
2523
import { getAppLogger } from '$lib/logger'
2624
@@ -220,9 +218,6 @@
220218
}
221219
222220
onMount(async () => {
223-
// Track dialog open state for MCP
224-
void notifyDialogOpened('copy-confirmation')
225-
226221
// Focus and select the path input
227222
await tick()
228223
pathInputRef?.focus()
@@ -236,9 +231,6 @@
236231
})
237232
238233
onDestroy(() => {
239-
// Track dialog close state for MCP
240-
void notifyDialogClosed('copy-confirmation')
241-
242234
// Cancel scan preview if still running
243235
if (previewId && isScanning) {
244236
void cancelScanPreview(previewId)
@@ -261,26 +253,27 @@
261253
}
262254
263255
function handleKeydown(event: KeyboardEvent) {
264-
event.stopPropagation()
265-
if (event.key === 'Escape') {
266-
handleCancel()
267-
} else if (event.key === 'Enter') {
256+
if (event.key === 'Enter') {
268257
handleConfirm()
269258
}
270259
}
271260
272261
function handleInputKeydown(event: KeyboardEvent) {
273-
event.stopPropagation()
274-
if (event.key === 'Escape') {
275-
handleCancel()
276-
} else if (event.key === 'Enter') {
262+
if (event.key === 'Enter') {
277263
event.preventDefault()
264+
event.stopPropagation()
278265
handleConfirm()
279266
}
280267
}
281268
</script>
282269

283-
<DraggableDialog titleId="dialog-title" onkeydown={handleKeydown}>
270+
<ModalDialog
271+
titleId="dialog-title"
272+
onkeydown={handleKeydown}
273+
dialogId="copy-confirmation"
274+
onclose={handleCancel}
275+
containerStyle="min-width: 420px; max-width: 500px"
276+
>
284277
{#snippet title()}{dialogTitle}{/snippet}
285278

286279
<!-- Direction indicator -->
@@ -368,7 +361,7 @@
368361
<button class="secondary" onclick={handleCancel}>Cancel</button>
369362
<button class="primary" onclick={handleConfirm}>Copy</button>
370363
</div>
371-
</DraggableDialog>
364+
</ModalDialog>
372365

373366
<style>
374367
.volume-selector {

0 commit comments

Comments
 (0)