refactor(server): Move the SSH import and discovery features from the server edit page to the settings page#1079
Conversation
… server edit page to the settings page
📝 WalkthroughWalkthroughRemoved SSH discovery, SSH-config import, and QR-scan import UI and handlers from the server edit page; simplified the first-time SSH-config auto-import check to a desktop-only, opt-in behavior. Moved full SSH import/discovery/QR flows into Settings: SSH config parsing with permission-fallback (file picker), SSH discovery navigation and username prompt, QR JSON→Spi import, deduplication and name-conflict resolution, confirmation dialogs, and import persistence via a new ServerDeduplication.importServersWithNotification(...). Added helper resolution logic, snackbar notifications, and updated GithubIds.participants. Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… configurations upon first launch Checks for and prompts the user to import SSH configurations upon the first launch on the desktop Optimized the SSH server import logic, adding duplicate detection and name conflict handling Fixed an issue with mount status checks that could occur during the import process
…onfiguration import features Move the QR code scanning feature from the server editing page to the settings page, and display different access points based on the platform Optimize the SSH configuration import logic to ensure the status is updated correctly after the configuration is read for the first time
Extract server import logic into the `ServerDeduplication` class Use the `importServersWithNotification` method consistently to handle imports Remove duplicate `_importServers` and `_resolveServers` methods Add checks for existing server IDs
- Moved the SSH configuration import logic from `edit.dart` to `actions.dart` - Removed redundant checks for the `mounted` parameter - Added handling for file permission exceptions - Improved logic for resolving server name conflicts
There was a problem hiding this comment.
🧹 Nitpick comments (3)
lib/view/page/server/edit/actions.dart (1)
186-191: Consider adding a hint about the Settings page for permission errors.On macOS with sandbox restrictions, users seeing "permission denied" may not know they can manually import from Settings. A slightly more informative message could help.
💡 Optional enhancement
if (e is PathAccessException || e.toString().contains('Operation not permitted')) { - context.showSnackBar(l10n.sshConfigPermissionDenied); + // Consider: l10n.sshConfigPermissionDeniedHint that mentions Settings > SSH + context.showSnackBar(l10n.sshConfigPermissionDenied); } else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/view/page/server/edit/actions.dart` around lines 186 - 191, The permission-denied branch should surface a hint directing users to the Settings import flow: when detecting PathAccessException or e.toString().contains('Operation not permitted'), update the message passed to context.showSnackBar (currently using l10n.sshConfigPermissionDenied) to include guidance about importing SSH config via the Settings page (or call a different localized string that appends that hint); modify the branch handling around PathAccessException / permission check to use the new/extended localized message so users on macOS know to open Settings → Import SSH config.lib/core/utils/server_dedup.dart (2)
85-93: Document the expected usage pattern for theresolvedServers-only path.When only
resolvedServersis passed and it's empty,source.lengthequals0, making theallExistMessage('0')semantically odd ("0 servers already exist"). The current callers handle this correctly by checking emptiness before calling, but this assumption isn't documented.Consider either:
- Adding a doc comment clarifying that callers should pre-filter empty lists when passing only
resolvedServers, or- Storing the original count separately if you want accurate "X already exist" messaging.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/core/utils/server_dedup.dart` around lines 85 - 93, The branch in server_dedup.dart that computes source = servers ?? resolvedServers! and then uses source.length for the allExistMessage produces misleading output when callers pass only resolvedServers (because source.length will be 0 even if resolvedServers was non-empty but resolved is empty); update the code or docs: either add a doc comment on the function (or the parameter resolvedServers) describing that callers must pre-filter and not pass an empty resolvedServers list, or change the logic in the method (around source, resolved, and the resolved.isEmpty check) to capture the original input count (e.g., store originalCount = (servers != null ? servers.length : resolvedServers!.length)) and use that count in context.showSnackBar(allExistMessage(...)) so the message reflects the original number; reference symbols: servers, resolvedServers, source, resolved, _resolveServers, context.showSnackBar, allExistMessage.
96-98: Consider batch import to reduce persistence overhead.Each
addServercall (lines 96-98) performs individual persistence writes viaStores.server.put(spi)andStores.setting.serverOrder.put(newOrder), resulting in 2N writes for N servers. For larger SSH config imports, this creates unnecessary I/O overhead. No batch add method currently exists in the codebase, but implementing one would consolidate these writes into a single operation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/core/utils/server_dedup.dart` around lines 96 - 98, The loop calling ref.read(serversProvider.notifier).addServer(server) causes 2N persistence writes (Stores.server.put and Stores.setting.serverOrder.put); replace with a new batch API on the servers notifier (e.g., addServers(List<Server>) or importServers(List<Server>)) that accepts the resolved list, performs in-memory state updates for all servers and computes/updates server order once, then does a single Stores.server.put (or bulk put) and a single Stores.setting.serverOrder.put; change the loop to a single call to this new batch method and ensure existing dedup/update logic from addServer is preserved inside the batch implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lib/core/utils/server_dedup.dart`:
- Around line 85-93: The branch in server_dedup.dart that computes source =
servers ?? resolvedServers! and then uses source.length for the allExistMessage
produces misleading output when callers pass only resolvedServers (because
source.length will be 0 even if resolvedServers was non-empty but resolved is
empty); update the code or docs: either add a doc comment on the function (or
the parameter resolvedServers) describing that callers must pre-filter and not
pass an empty resolvedServers list, or change the logic in the method (around
source, resolved, and the resolved.isEmpty check) to capture the original input
count (e.g., store originalCount = (servers != null ? servers.length :
resolvedServers!.length)) and use that count in
context.showSnackBar(allExistMessage(...)) so the message reflects the original
number; reference symbols: servers, resolvedServers, source, resolved,
_resolveServers, context.showSnackBar, allExistMessage.
- Around line 96-98: The loop calling
ref.read(serversProvider.notifier).addServer(server) causes 2N persistence
writes (Stores.server.put and Stores.setting.serverOrder.put); replace with a
new batch API on the servers notifier (e.g., addServers(List<Server>) or
importServers(List<Server>)) that accepts the resolved list, performs in-memory
state updates for all servers and computes/updates server order once, then does
a single Stores.server.put (or bulk put) and a single
Stores.setting.serverOrder.put; change the loop to a single call to this new
batch method and ensure existing dedup/update logic from addServer is preserved
inside the batch implementation.
In `@lib/view/page/server/edit/actions.dart`:
- Around line 186-191: The permission-denied branch should surface a hint
directing users to the Settings import flow: when detecting PathAccessException
or e.toString().contains('Operation not permitted'), update the message passed
to context.showSnackBar (currently using l10n.sshConfigPermissionDenied) to
include guidance about importing SSH config via the Settings page (or call a
different localized string that appends that hint); modify the branch handling
around PathAccessException / permission check to use the new/extended localized
message so users on macOS know to open Settings → Import SSH config.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e156c2fe-7dcb-46a7-abdd-6cba54285bc5
📒 Files selected for processing (4)
lib/core/utils/server_dedup.dartlib/view/page/server/edit/actions.dartlib/view/page/server/edit/edit.dartlib/view/page/setting/entries/ssh.dart
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/view/page/setting/entries/ssh.dart
…n import - Modified the format of the import success message to display the number of servers successfully imported - Added a prompt for manual selection when permissions are denied - Optimized the server deduplication logic to display an “already exists” message based on the original count
…nfigurations Adjusted the server's deduplication logic to ensure the correct original count is used when displaying the number of imports Removed unnecessary flag settings for the first read of SSH configurations
There was a problem hiding this comment.
♻️ Duplicate comments (2)
lib/view/page/setting/entries/ssh.dart (1)
130-136:⚠️ Potential issue | 🟡 MinorLocalize the discovery success copy as a single message.
Line 135 builds the snackbar text from raw fragments, which hard-codes English word order and skips pluralization. Please reuse one parameterized l10n entry here, or add a dedicated discovery-import message if none already fits.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/view/page/setting/entries/ssh.dart` around lines 130 - 136, The snackbar success text passed into ServerDeduplication.importServersWithNotification uses concatenated fragments (importedMessage: (count) => '${libL10n.success}: $count ${libL10n.servers}') which hardcodes word order and ignores pluralization; update the call to pass a single localized, parameterized string from your l10n (or add a new entry like discoveryImportSuccess(count)) and use that key for importedMessage, keeping the same arguments (servers, ref, context, allExistMessage) and preserving pluralization rules in the localization file.lib/view/page/server/edit/actions.dart (1)
141-194:⚠️ Potential issue | 🟡 MinorClear
firstTimeReadSSHCfgon every exit path.Line 145, Line 169, and the permission-denied branch all exit before the flag is updated, so this “first-time” check can keep running on later visits to the edit page. Moving
Stores.setting.firstTimeReadSSHCfg.put(false)to right after the first parse attempt finishes, or into afinally, avoids the repeat prompt/snackbar paths.Suggested fix
Future<void> _checkSSHConfigImport() async { try { final servers = await SSHConfig.parseConfig(); + Stores.setting.firstTimeReadSSHCfg.put(false); if (!mounted) return; if (servers.isEmpty) { return; } @@ } catch (e) { + Stores.setting.firstTimeReadSSHCfg.put(false); if (!mounted) return; if (e is PathAccessException || e.toString().contains('Operation not permitted')) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/view/page/server/edit/actions.dart` around lines 141 - 194, The _checkSSHConfigImport method fails to clear the firstTimeReadSSHCfg flag on all exit paths; move the Stores.setting.firstTimeReadSSHCfg.put(false) call into a finally block (or execute it immediately after the SSHConfig.parseConfig() call) so it always runs regardless of early returns or exceptions; update references in this function (remove the existing scattered calls at the top/inside the try and inside the catch) and ensure the finally runs after SSHConfig.parseConfig(), preserving the existing behavior for showing dialogs/snackbars and the ServerDeduplication.importServersWithNotification call.
🧹 Nitpick comments (1)
lib/view/page/setting/entries/ssh.dart (1)
23-261: Split the new SSH settings flow into build/actions helpers.This extension now owns tiles, scanner/discovery handlers, dialogs, file picking, parsing, and import orchestration. Pulling the
_onTap*,_process*, and_handle*methods into companion actions/utils extensions would keep the settings entry easier to scan and aligned with the repo’s view structure. As per coding guidelines, "Split UI into build, actions, and utils; use extension on to separate concerns".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/view/page/setting/entries/ssh.dart` around lines 23 - 261, The settings entry is doing too much—extract the action and util logic out of the widget file by moving the tap handlers and processing helpers into companion extensions/helpers: create an extension or separate helper class that implements _onTapQrScan, _onTapSSHDiscovery, _processDiscoveredServers, _onTapSSHConfigImport, _processSSHServers, _handleImportSSHCfgPermissionIssue, and _onTapSSHImportWithFilePicker (keeping the same signatures) and have the widget file only call those methods; ensure you inject or pass needed context/ref dependencies into the new helpers (or accept BuildContext and WidgetRef parameters) and update imports and references so the ListTile builders (_buildSSHConfigImport, _buildQrScan, _buildSSHDiscovery) only contain UI code and forward actions to the extracted helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@lib/view/page/server/edit/actions.dart`:
- Around line 141-194: The _checkSSHConfigImport method fails to clear the
firstTimeReadSSHCfg flag on all exit paths; move the
Stores.setting.firstTimeReadSSHCfg.put(false) call into a finally block (or
execute it immediately after the SSHConfig.parseConfig() call) so it always runs
regardless of early returns or exceptions; update references in this function
(remove the existing scattered calls at the top/inside the try and inside the
catch) and ensure the finally runs after SSHConfig.parseConfig(), preserving the
existing behavior for showing dialogs/snackbars and the
ServerDeduplication.importServersWithNotification call.
In `@lib/view/page/setting/entries/ssh.dart`:
- Around line 130-136: The snackbar success text passed into
ServerDeduplication.importServersWithNotification uses concatenated fragments
(importedMessage: (count) => '${libL10n.success}: $count ${libL10n.servers}')
which hardcodes word order and ignores pluralization; update the call to pass a
single localized, parameterized string from your l10n (or add a new entry like
discoveryImportSuccess(count)) and use that key for importedMessage, keeping the
same arguments (servers, ref, context, allExistMessage) and preserving
pluralization rules in the localization file.
---
Nitpick comments:
In `@lib/view/page/setting/entries/ssh.dart`:
- Around line 23-261: The settings entry is doing too much—extract the action
and util logic out of the widget file by moving the tap handlers and processing
helpers into companion extensions/helpers: create an extension or separate
helper class that implements _onTapQrScan, _onTapSSHDiscovery,
_processDiscoveredServers, _onTapSSHConfigImport, _processSSHServers,
_handleImportSSHCfgPermissionIssue, and _onTapSSHImportWithFilePicker (keeping
the same signatures) and have the widget file only call those methods; ensure
you inject or pass needed context/ref dependencies into the new helpers (or
accept BuildContext and WidgetRef parameters) and update imports and references
so the ListTile builders (_buildSSHConfigImport, _buildQrScan,
_buildSSHDiscovery) only contain UI code and forward actions to the extracted
helpers.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ca480663-d5b3-4850-b602-590a3b77417a
📒 Files selected for processing (3)
lib/core/utils/server_dedup.dartlib/view/page/server/edit/actions.dartlib/view/page/setting/entries/ssh.dart
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/core/utils/server_dedup.dart
…SSH configuration access was denied When SSH configuration access is denied, set the “first read” flag to false to prevent repeated prompts
…mporting SSH configurations Moved the logic for checking existing servers to an earlier stage to avoid unnecessary parsing of SSH configurations
| final hasExistingServers = ref.read(serversProvider).servers.isNotEmpty; | ||
| if (hasExistingServers) { | ||
| Stores.setting.firstTimeReadSSHCfg.put(false); | ||
| return; |
There was a problem hiding this comment.
🟡 Auto-import toggle silently disabled by hasExistingServers check, making the settings toggle a no-op
The _buildSSHConfigAutoImportToggle in lib/view/page/setting/entries/server.dart:268 lets users toggle firstTimeReadSSHCfg back on. However, _checkSSHConfigImport at lib/view/page/server/edit/actions.dart:142-145 immediately sets it to false if serversProvider.servers.isNotEmpty, and returns without performing any action. This means any user who already has servers can flip the toggle on in Settings, but as soon as they navigate to the server edit page (to add a new server), the toggle is silently flipped back off with no SSH config import prompt shown. The user sees their setting reset without explanation.
The old code only checked the flag value (if (!prop.fetch()) return) and did not have the hasExistingServers guard, so re-enabling the toggle would actually re-trigger the auto-import flow.
Prompt for agents
The `hasExistingServers` check at lib/view/page/server/edit/actions.dart:142-145 causes the settings toggle at lib/view/page/setting/entries/server.dart:264-269 (_buildSSHConfigAutoImportToggle) to be a no-op for users who already have servers. There are two options to fix this:
1. Remove the `hasExistingServers` check entirely, reverting to the old behavior where re-enabling the toggle actually re-triggers the auto-import:
- Delete lines 142-146 in lib/view/page/server/edit/actions.dart
2. Keep the `hasExistingServers` check but make the toggle unavailable when servers exist:
- In lib/view/page/setting/entries/server.dart, modify _buildSSHConfigAutoImportToggle to disable the toggle or hide it when the user already has servers (e.g., check ref.read(serversProvider).servers.isNotEmpty and show a disabled state or explanatory text).
Was this helpful? React with 👍 or 👎 to provide feedback.
These two features are related to import management operations across multiple servers and should not be placed under the existing single-server details page.
It would be better to move them to the general settings page, which would also resolve the issue of the three buttons being displayed incorrectly on the desktop version.
Resolve #950.
Summary by CodeRabbit
Refactor
New Features
Bug Fixes / UX
Chores