Files
Firelink/INTERACTION_REVIEW.md
T
NimBold 038f31b988 feat(settings): modernize download location settings
- Replace split 'Default Download Path' and 'All Categories Base' with a canonical baseDownloadFolder and categorySubfolders model
- Retain absolute category overrides only where required
- Update Add window to accurately reflect intended destination:
  - Single URL: specific category path
  - Multiple URLs: base folder with explanatory text
  - Manual Browse override: selected folder
- Centralize path resolution logic shared by Rust backend and UI
2026-06-20 19:22:20 +03:30

34 KiB
Raw Blame History

Firelink — UI Interaction Inventory

A read-only audit of every user-facing action in Firelink, mapping each to the component/handler, Zustand store action, IPC command, Rust function, expected behavior, and the likely validation method. No code was changed.

Legend:

  • = wired end-to-end (UI → store → IPC → Rust)
  • ⚠️ = present in UI/store but with a gap, mismatch, or risk (see notes)
  • = declared/wired in one layer but missing in another (dead path)

1. Main Window

1.1 Title bar / Toolbar (DownloadTable.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
M1 Add Download () DownloadTabletoggleAddModal(true) useDownloadStore.toggleAddModal Opens Add Downloads modal None
M2 Resume All (▶) DownloadTable maps over paused items → handleResume useDownloadStore.resumeDownload (per item) resume_download lib.rs:2211 resume_download Resumes every paused download in the current filter Per-item status check
M3 Pause All (⏸) DownloadTable maps over downloading items → handlePause — (direct) pause_download lib.rs:2137 pause_download Pauses every downloading item in the current filter Per-item aria2 status
M4 Show Sidebar (PanelLeft, when hidden) DownloadTabletoggleSidebar useSettingsStore.toggleSidebar Reveals sidebar; persisted None
M5 Drag window WindowDragRegion / data-tauri-drag-region divs → getCurrentWindow().startDragging() core window Tauri core Moves window Native
M6 Sidebar resize handle App.startSidebarResize (pointer drag) setSidebarWidth (local) → localStorage Resizes sidebar 190260px Clamped in handler

⚠️ M2/M3: "Resume All"/"Pause All" only act on items in the current sidebar filter (e.g. active vs. all). A user on the "Active" filter pausing all won't touch paused items elsewhere — could surprise users who read the button as global. Contrast with the tray's true global Pause/Resume All (downloadStore.ts:79).

1.2 Download row actions (DownloadItem.tsx, DownloadTable.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
M7 Move Up / Move Down (queued) DownloadItemmoveInQueue(id,'up'|'down') useDownloadStore.moveInQueue move_in_queue lib.rs:2613 move_in_queue Reorders pending queue; disabled at bounds queueIndex === 0 / length-1
M8 Pause (row, active) DownloadItem.handlePause pause_download pause_download Pauses aria2/native/media task aria2 status check
M9 Resume (row, paused) DownloadItem.handleResume resumeDownload resume_download resume_download Resumes; re-enqueues if no gid gid presence
M10 Options menu (⋮) / right-click DownloadItemsetContextMenu Opens context menu
M11 Double-click row handleDownloadDoubleClick open_downloaded_file commands.rs:27 open_downloaded_file Completed → open file; else → Properties item.status === 'completed'

1.3 Row context menu (DownloadTable.tsx, floating menu)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
C1 Open openDownloadFile open_downloaded_file open_downloaded_file Opens completed file status==='completed'; path authorization
C2 Show in Finder revealDownloadFile reveal_in_file_manager commands.rs:6 reveal_in_file_manager Reveals file/part in Finder status==='completed'; path authorization
C3 Pause handlePause pause_download pause_download Pauses status in {downloading,queued,retrying}
C4 Resume handleResume resumeDownload resume_download resume_download Resumes status in {paused,failed,retrying}
C5 Redownload redownload(contextItem.id) useDownloadStore.redownload enqueue_download (via dispatchItem) enqueue_download Creates a new queued copy status in {completed,failed,paused}
C6 Copy Address navigator.clipboard.writeText(url) Copies URL try/catch toast
C7 Copy File Path clipboard write Copies resolved path status==='completed'; filename present
C8 Remove handleDeleteopenDeleteModal(id) useDownloadStore.openDeleteModal Opens Delete modal
C9 Properties openPropertiessetSelectedPropertiesDownloadId useDownloadStore.setSelectedPropertiesDownloadId Opens Properties modal

1.4 Column resize

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
M12 Resize column handle DownloadTable.startColumnResize setColumnWidths (local) Resizes table columns per-column minimums

1.5 Sidebar (Sidebar.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
S1 Library filters (All/Active/Completed/Unfinished) NavItemonSelectFilter App.setFilter + setActiveView('downloads') Filters download list
S2 Folder filters (Musics/Movies/…/Other) NavItem as above Filters by category
S3 Queue item select QueueItemonSelectFilter('queue:<id>') as above Filters by queueId
S4 Add new queue "Add new queue" button → input → handleAddQueueSubmit useDownloadStore.addQueue Adds queue (persisted to store.bin) name trimmed non-empty
S5 Queue rename context → Rename → handleRenameQueueSubmit useDownloadStore.renameQueue Renames queue name trimmed non-empty
S6 Queue delete context → Delete useDownloadStore.removeQueue Deletes queue; reassigns downloads to Main id !== MAIN_QUEUE_ID
S7 Start Queue context → Start Queue useDownloadStore.startQueue enqueue_download/resume_download dispatchItem Dispatches/resumes queue's runnable items queued/paused/failed
S8 Pause Queue context → Pause Queue useDownloadStore.pauseQueue pause_download pause_download Pauses all downloading in queue status downloading
S9 Tools: Scheduler ToolItem view='scheduler' setActiveView('scheduler') Shows Scheduler view
S10 Tools: Speed Limiter ToolItem view='speedLimiter' setActiveView('speedLimiter') Shows Speed Limiter view
S11 Tools: Diagnostics ToolItem view='diagnostics' setActiveView('diagnostics') Shows Diagnostics console
S12 Settings footer button setActiveView('settings') Shows Settings view
S13 Hide Sidebar toggleSidebar useSettingsStore.toggleSidebar Collapses sidebar

1.6 Global handlers (App-level useEffect listeners)

# Action Trigger Handler Store/IPC Rust fn Expected behavior Validation
G1 Paste URLs window paste (non-input) extractValidDownloadUrlsopenAddModalWithUrls store Opens Add modal with pasted URLs URL scheme allowlist
G2 Deep link deep-link-add-download event openAddModalWithUrls(payload) store dispatch_deep_linksparse_firelink_urls Opens Add modal scheme/host/length caps
G3 Extension add extension-add-download event handleExtensionDownload store extension_server Opens Add modal dedupe URLs
G4 Extension queued batch extension-downloads-queued event merges into downloads/pendingOrder setState extension_server Appends new items id dedupe
G5 Schedule trigger schedule-trigger event ('start'/'stop') startQueue/pauseQueue + setSchedulerRunning store+IPC scheduler.rs Starts/stops main queue
G6 Post-queue system action scheduler end + postQueueAction !== 'none' invoke('perform_system_action') lib.rs:2555 perform_system_action Sleep/restart/shutdown automation permission
G7 Notification download-state terminal sendNotification OS notification permission; showNotifications

2. Add Download Window (AddDownloadsModal.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
A1 Paste/edit URLs textarea setUrls (local) → debounce parse fetch_metadata / fetch_media_metadata (via fetchMediaMetadataDeduped) lib.rs:616 fetch_metadata, lib.rs:784 fetch_media_metadata Resolves filename/size/formats new URL(); isMediaUrl
A2 Refresh Metadata setMetadataRefreshNonce(n+1) (re-runs effect) as above as above Re-fetches metadata active guard
A3 Select preview row setSelectedItemIndex(i) Selects item for media-format panel bounds
A4 Select media format selectMediaFormat(idx) — (local parsedItems) Sets chosen stream; updates filename/size selectedFormat bounds
A5 Browse Save Location handleBrowseopen({directory:true}) Sets save location; marks manual dialog result
A6 Target Queue select setSelectedQueueId Sets target queue queue exists
A7 Connections slider setConnections 116 (disabled for media) min/max
A8 Limit speed per file (checkbox+input) setSpeedLimitEnabled/setSpeedLimit Per-file KiB/s cap numeric
A9 Use authorization (checkbox+user/pass) setUseAuth/setUsername/setPassword Sends basic auth useAuth flag
A10 Advanced toggle setAdvancedExpanded Expands advanced fields
A11 Verify Checksum (algo+digest) setChecksumEnabled/setChecksumAlgo/setChecksumValue Sends algo=digest checksumEnabled
A12 Headers / Cookies / Mirrors setHeaders/setCookies/setMirrors Raw strings forwarded trim/empty
A13 Add to Queue handleStart(false)executeAddDownloads(false,…) addDownload (status 'paused') enqueue_download (via dispatchItem) enqueue_download Adds paused items duplicate detection
A14 Start Downloads handleStart(true)executeAddDownloads(true,…) addDownload (status 'queued') + immediate dispatch enqueue_download enqueue_download Adds + starts items duplicate detection
A15 Cancel toggleAddModal(false) useDownloadStore.toggleAddModal Closes modal

Duplicate detection (handleStartDuplicateResolutionModal):

  • Checks URL already in queue (status !== failed/completed) and file exists on disk via check_file_exists and in-store path match.
  • DuplicateResolutionModal: per-conflict Rename / Replace (file only) / Skip select → on Confirm calls executeAddDownloads(…,resolutions).
# Action Component / fn Store/IPC Rust fn Expected behavior Validation
A16 Pick resolution per conflict updateResolution (local) rename/replace/skip reason.type gates replace
A17 Continue onConfirm(resolutions)executeAddDownloads store+IPC enqueue_download; delete_file (replace) Applies resolutions loop bounds

⚠️ A14 "Start Downloads" does not show any progress/confirmation that duplicate conflicts were resolved; resolution loop uses check_file_exists only to gate 'rename', but the rename loop caps at count < 1000 and silently keeps the last tried name if exceeded.


3. Download Properties Window (PropertiesModal.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
P1 Edit URL setUrl Saves on Save non-empty on save
P2 Edit File name setFileName Saves on Save non-empty on save
P3 Select Save Location handleBrowseopen({directory}) Sets destination dialog result
P4 Connections setConnections (116) per-file connections disabled if transferLocked
P5 Limit speed (checkbox+value) setSpeedLimitEnabled/setSpeedLimitValue per-file KiB/s disabled if transferLocked
P6 Login mode toggle (matching/custom/none) setLoginMode Credentials source disabled if transferLocked
P7 Custom username/password setUsername/setPassword Override login custom mode only
P8 Checksum (verify+algo+digest) setChecksumEnabled/setChecksumAlgorithm/setChecksumValue algo=digest disabled if transferLocked
P9 Cookies / Headers / Mirrors setCookies/setHeaders/setMirrors Advanced fields disabled if transferLocked
P10 Advanced toggle setAdvancedExpanded Expand/collapse
P11 Save handleSaveapplyProperties(id, updates) useDownloadStore.applyProperties remove_from_queue/detach_download_for_reconfigure/enqueue_download (re-dispatch) remove_from_queue, detach_download_for_reconfigure, enqueue_download Updates item; re-attaches backend status guard (throws if active)
P12 Cancel setSelectedPropertiesDownloadId(null) as above Closes modal

applyProperties rules (useDownloadStore.ts:313):

  • downloading/processing/retrying → throws "Cannot change properties while transfer is active."
  • completed/failed → updates store only (read-only identity; settings saved for redownload).
  • queued → removes from queue, updates, re-dispatches; on dispatch failure removes from queue.
  • pauseddetach_download_for_reconfigure, unregister, update (no re-dispatch; resumes on next resume).

⚠️ P5/P8/P9 use isTransferLocked (downloading/processing/retrying) to disable inputs, but isLocked also includes completed. For a completed item the URL/filename/destination are disabled (correct), yet speed-limit, checksum, cookies, headers, mirrors are editable — these only matter for redownload, which is the documented intent, but the disable logic is split across two booleans (isLocked vs isTransferLocked) and is easy to misread.


4. Settings Window (SettingsView.tsx, tabbed)

# Action Tab Store setter IPC command Rust fn Validation
T1 Tab select setActiveSettingsTab enum
Downloads
SD1 Default connections (116) downloads setPerServerConnections onBlur clamp 116
SD2 Parallel downloads (112) downloads setMaxConcurrentDownloads set_concurrent_limit (App effect) set_concurrent_limit onBlur clamp 112
SD3 Global speed limit downloads setGlobalSpeedLimit set_global_speed_limit (App effect) set_global_speed_limit free text
SD4 Automatic retries (010) downloads setMaxAutomaticRetries onBlur clamp 010
SD5 Show notification on completion downloads setShowNotifications toggle
SD6 Play sound on completion downloads setPlayCompletionSound requires SD5
Look & Feel
SL1 Theme (system/light/dark/dracula/nord) lookandfeel setTheme App effect applies classes
SL2 Font size lookandfeel setAppFontSize data-font-size attr
SL3 List row density lookandfeel setListRowDensity enum
SL4 Dock badge lookandfeel setShowDockBadge update_dock_badge (setter + effect) update_dock_badge toggle; clears badge when off
SL5 Menu bar icon lookandfeel setShowMenuBarIcon toggle_tray_icon (App effect) toggle_tray_icon toggle
Network
SN1 Proxy mode (none/system/custom) network setProxyMode radio
SN2 Proxy host network setProxyHost shown if custom
SN3 Proxy port (165535) network setProxyPort onBlur clamp
SN4 Custom User Agent network setCustomUserAgent free text + datalist
Locations
SO1 Default download path (text+Browse) locations setDefaultDownloadPath pickDirectory
SO2 Ask where to save each file locations setAskWhereToSaveEachFile toggle
SO3 All Categories Base Browse locations setCategoryDirectory ×7 create_category_directories parity::create_category_directories creates dirs on disk
SO4 Per-category path (text+Browse) locations setCategoryDirectory handleBrowseCategory
SO5 Reset Defaults locations resetCategoryDirectories restores defaults
Site Logins
SLg1 Delete login sitelogins removeSiteLogin delete_keychain_password delete_keychain_password removes keychain entry
SLg2 Add login (pattern/user/pass) sitelogins addSiteLogin set_keychain_password set_keychain_password pattern+user non-empty
Power
SP1 Prevent sleep while downloading power setPreventsSleepWhileDownloading set_prevent_sleep (syncSystemIntegrations + setter) set_prevent_sleep keepawake on/off
Engine
SE1 Recheck engines engine get_*_engine_status ×4 check_* force re-run; cached otherwise
SE2 Show/hide technical details engine — (local) expand card
SE3 Browser Cookies Source engine setMediaCookieSource enum (none/safari/chrome/firefox/edge/brave)
Integrations
SI1 Copy Token integrations — (clipboard) copies pairing token
SI2 Regenerate token integrations regeneratePairingToken set_keychain_password + set_extension_pairing_token (App effect) both mints new token
SI3 Get Extension links integrations — (anchor) opens external URLs
About
SA1 Check for Updates about check_for_updates parity::check_for_updates toast result
SA2 Auto-check updates toggle about setAutoCheckUpdates switch

5. Tools Views

5.1 Scheduler (SchedulerView.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
SC1 Enable Scheduler updateDraft('enabled') — (draft) Toggles draft (saved on Save)
SC2 Start/Stop time updateDraft('startTime'/'stopTime'/'stopTimeEnabled') Sets schedule window time input
SC3 Run Every Day updateDraft('everyday') All days
SC4 Day toggles toggleDay Selected days
SC5 Post-queue action radio updateDraft('postQueueAction') none/sleep/restart/shutdown
SC6 Run Now runNowstartQueue(MAIN_QUEUE_ID) setSchedulerRunning(true) enqueue/resume dispatchItem Starts main queue now count>0
SC7 Pause pauseNowpauseQueue(MAIN_QUEUE_ID) setSchedulerRunning(false) pause_download pause_download Pauses main queue
SC8 Save Settings savesetScheduler(normalized) setScheduler Persists scheduler; registered by scheduler.rs selectedDays fallback
SC9 Grant/Revoke Automation handlePermissionAction request_automation_permission / open_automation_settings both macOS Automation for Finder platform isMac

5.2 Speed Limiter (SpeedLimiterView.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
VL1 Enable toggle setEnabled — (local) Enables draft
VL2 Value input setValue numeric min 1; clamp
VL3 Unit KB/s · MB/s setUnit unit select
VL4 Quick presets (1/5/10 MB/s) preset(n) sets value+unit
VL5 Save Limit savesetGlobalSpeedLimit + setLastCustomSpeedLimitKiB both set_global_speed_limit (App effect) set_global_speed_limit Applies global cap clamp; disabled→''

5.3 Diagnostics (DiagnosticsView.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
DG1 Clear console handleClearsetLogs([]) — (local) Empties log view
DG2 Export Logs handleExportsave({…}) export_logs lib.rs:2800 export_logs Writes log to chosen path path from save dialog

6. Menus / Tray

6.1 Tray menu (built twice — see risk note)

Primary tray built in setup() (lib.rs:3192) with items: Show Firelink, Pause All, Resume All, Quit. Menu events emit tray-action events handled by downloadStore.ts:79 (true global pause/resume across all queues). Left-click tray icon restores main window.

toggle_tray_icon (lib.rs:2822) rebuilds the tray when "Menu bar icon" is toggled, but only registers Show / Quit — it omits the Pause All / Resume All items. Because toggle_tray_icon checks tray_by_id("main") and the setup tray already used id "main", toggling on is a no-op while the setup tray exists.

# Action Trigger Handler IPC / Event Rust fn Expected behavior
TR1 Show Firelink tray menu restore_main_window restore_main_window Unminimize/show/focus
TR2 Pause All tray menu emits tray-action 'pause-all' event Pauses all queues
TR3 Resume All tray menu emits tray-action 'resume-all' event Starts all queues
TR4 Quit tray menu app.exit(0) Exits app
TR5 Tray left-click icon click restore_main_window Show window

⚠️ There is no native macOS app menu (no Edit menu with Cut/Copy/Paste / Cmd+Q, no Window menu). Standard keyboard shortcuts and clipboard editing rely on the webview default. This is a UX gap on macOS.

6.2 Window controls

Close/minimize/maximize are the native Tauri decorations (title bar hidden; WindowDragRegion provides drag only). Close behavior default (no minimize-to-tray override observed).


7. Delete Confirmation (DeleteConfirmationModal.tsx)

# Action Component / fn Store action IPC command Rust fn Expected behavior Validation
D1 Remove (from list only) handleRemoveFromListremoveDownload(id,false) useDownloadStore.removeDownload remove_download (filepath null) remove_download Removes item; keeps file isRemoving flag
D2 Delete file handleDeleteFileremoveDownload(id,true) removeDownload trash_download_assets + remove_download trash_download_assets, remove_download Trashes file+.aria2+.part, removes item path resolve
D3 Cancel handleCancelcloseDeleteModal closeDeleteModal Closes modal

8. IPC Command → Rust function map

All commands declared in src/ipc.ts CommandMap. Registered handlers: src-tauri/src/lib.rs:3550 generate_handler![…].

IPC command Rust fn File:line Status
fetch_metadata fetch_metadata lib.rs:616
fetch_media_metadata fetch_media_metadata lib.rs:784
get_engine_status get_engine_status lib.rs:1674
get_aria2_engine_status get_aria2_engine_status lib.rs:1694
get_ytdlp_engine_status get_ytdlp_engine_status lib.rs:1702
get_ffmpeg_engine_status get_ffmpeg_engine_status lib.rs:1707
get_deno_engine_status get_deno_engine_status lib.rs:1712
open_file open_file lib.rs:1018 (unused by UI; UI uses open_downloaded_file)
show_in_folder show_in_folder lib.rs:1031 (unused by UI; UI uses reveal_in_file_manager)
reveal_in_file_manager commands::reveal_in_file_manager commands.rs:6
open_downloaded_file commands::open_downloaded_file commands.rs:27
trash_download_assets commands::trash_download_assets commands.rs:45
pause_download pause_download lib.rs:2137
resume_download resume_download lib.rs:2211
remove_download remove_download lib.rs:2292
detach_download_for_reconfigure detach_download_for_reconfigure lib.rs:2364
enqueue_download enqueue_download lib.rs:2565
enqueue_many enqueue_many lib.rs:2586
move_in_queue move_in_queue lib.rs:2613
remove_from_queue remove_from_queue lib.rs:2622
get_pending_order get_pending_order lib.rs:2560
set_concurrent_limit set_concurrent_limit lib.rs:2636
set_global_speed_limit set_global_speed_limit lib.rs:2663
update_dock_badge update_dock_badge lib.rs:2507
set_prevent_sleep set_prevent_sleep lib.rs:2526
perform_system_action perform_system_action lib.rs:2555
request_automation_permission request_automation_permission lib.rs:2680
open_automation_settings open_automation_settings lib.rs:2707
get_free_space get_free_space lib.rs:2721
set_keychain_password set_keychain_password lib.rs:2758
get_keychain_password get_keychain_password lib.rs:2765
delete_keychain_password delete_keychain_password lib.rs:2771
check_file_exists check_file_exists lib.rs:2778
delete_file delete_file lib.rs:2787
toggle_tray_icon toggle_tray_icon lib.rs:2823 ⚠️ see TR note
set_extension_pairing_token set_extension_pairing_token lib.rs:2875
set_extension_frontend_ready set_extension_frontend_ready lib.rs:2892
get_system_proxy parity::get_system_proxy parity.rs:7
get_file_category parity::get_file_category parity.rs:22 (UI uses TS categoryForFileName instead)
check_for_updates parity::check_for_updates parity.rs:84
is_supported_media parity::is_supported_media parity.rs:180 (declared in ipc.ts but UI uses TS isMediaUrl/get_supported_media_domains)
create_category_directories parity::create_category_directories parity.rs:144
export_logs export_logs lib.rs:2800
db_save_settings declared in ipc.ts but not registered in Rust
db_load_settings declared in ipc.ts but not registered in Rust
db_get_all_downloads declared in ipc.ts but not registered in Rust
db_save_download declared in ipc.ts but not registered in Rust
db_delete_download declared in ipc.ts but not registered in Rust
start_download declared in ipc.ts (StartDownloadArgs) but not registered in Rust
start_media_download declared in ipc.ts (StartMediaDownloadArgs) but not registered in Rust
get_supported_media_domains parity::get_supported_media_domains parity.rs:175 (called directly via invoke in downloads.ts, not in typed CommandMap)

Persistence actually runs through the Tauri plugin-store (LazyStore on store.bin) in the Zustand subscriber (useDownloadStore.ts:646), not the db_* commands — those are dead declarations. Likewise start_download / start_media_download are superseded by enqueue_download/enqueue_many via the dispatchItem flow.

Backend events (Rust → Frontend)

Event Payload Emitted by Listened by
download-progress DownloadProgressEvent download/queue tasks downloadStore.ts:31
download-state DownloadStateEvent pause/resume/remove + tasks downloadStore.ts:49, App.tsx:210
download-complete / download-failed string (declared; not emitted in read code)
schedule-trigger 'start'|'stop' scheduler.rs App.tsx:113
tray-action 'pause-all'|'resume-all' tray menu downloadStore.ts:80
extension-add-download ExtensionDownload extension_server App.tsx:231
extension-downloads-queued DownloadItem[] extension_server App.tsx:234
deep-link-add-download string deep-link dispatch App.tsx:251
log {level,message} tauri-plugin-log DiagnosticsView.tsx:19

9. Highest-Risk Areas

Ranked by likelihood × impact:

  1. Tray menu divergence / duplicate tray builder (lib.rs:3192 vs lib.rs:2822). The setup tray registers Pause All / Resume All; the toggle builder does not and silently no-ops when the setup tray already exists. Toggling "Menu bar icon" off then on can leave the user with a tray that has no pause/resume items, or no tray at all depending on ordering. Risk: lost global queue control + confusing state.

  2. Resume/Pause All scope mismatch (M2/M3 vs TR2/TR3). Toolbar Resume/Pause All only act on the current filter, while tray Resume/Pause All act on all queues. Same icon, different semantics — users will mis-pause and leave downloads running, or fail to resume paused items.

  3. perform_system_action (Sleep/Restart/Shutdown) is invoked with no confirmation dialog from the scheduler path (App.tsx:139) once the queue drains. A user who enables "Shut down" and walks away loses unsaved work in other apps. Permission is checked, but no undo. Risk: destructive, cross-application.

  4. applyProperties state machine (useDownloadStore.ts:313). Five branches (active throws, completed/failed store-only, queued re-dispatch, paused detach) with backend registration tracked in a separate Set. A failed detach_download_for_reconfigure throws and preserves old props, but a failed re-dispatch on queued silently removes from queue. Easy to reach a state where the UI shows "queued" but the backend has no task (backendRegisteredIds desync).

  5. dispatchItem in-flight dedupe + failure → 'failed' (useDownloadStore.ts:21). On enqueue failure the item is marked failed without surfacing the error to the user (only console.error). Users see a mysteriously failed download with no diagnostic.

  6. Path authorization surface (commands.rs). open_downloaded_file, reveal_in_file_manager, trash_download_assets all gate on known_download_paths (registered via download_ownership). If an item is redownloaded or its destination changes, stale registration could either block a legitimate open or — worse — the delete_file path used by duplicate "Replace" (AddDownloadsModal A17) bypasses ownership checks and uses is_safe_path only. Risk: deleting the wrong file if a crafted path passes is_safe_path.

  7. Dead IPC declarations (start_download, start_media_download, all db_*). Not exploitable, but a maintenance trap: any future caller using the typed invokeCommand for these will get a runtime "command not found" with no compile-time signal.

  8. No native macOS app menu. No Edit menu (cut/copy/paste/select-all), Cmd+Q, or Window menu. Clipboard paste into the Add-URLs textarea works only via the webview default; the global paste handler (G1) intentionally ignores inputs, so Cmd+V in the textarea is the only path — fine, but no menu affordance.

  9. Duplicate-resolution rename loop (A17) caps at 1000 attempts and on exhaustion keeps the last tried name without warning, which could collide and overwrite via the subsequent addDownload if the backend doesn't guard.

  10. SpeedLimiterView Save applies via effect (App.tsx:101), not directly. The previousSpeedLimit ref dedupes, but the indirection means the visible toast ("Global limit saved…") can fire before the backend actually accepts the limit, and a failed set_global_speed_limit is only logged.