Skip to content

refactor(server): Move the SSH import and discovery features from the server edit page to the settings page#1079

Merged
GT-610 merged 9 commits intolollipopkit:mainfrom
GT-610:entry-move
Mar 20, 2026
Merged

refactor(server): Move the SSH import and discovery features from the server edit page to the settings page#1079
GT-610 merged 9 commits intolollipopkit:mainfrom
GT-610:entry-move

Conversation

@GT-610
Copy link
Copy Markdown
Collaborator

@GT-610 GT-610 commented Mar 20, 2026

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

    • Moved SSH/QR import and discovery UI out of the server edit page into Settings; edit page now shows only the write-script tip and only triggers desktop auto-import when enabled.
  • New Features

    • Settings: desktop SSH-config auto-import toggle, manual SSH-config import (with file-picker), QR-scan import, and network SSH discovery with preview and conflict-resolution.
    • Import helper that deduplicates, resolves names, imports servers, and shows success feedback.
  • Bug Fixes / UX

    • Streamlined first-time import prompts and improved permission/error messaging.
  • Chores

    • Added a contributor GitHub handle.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

Removed 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

  • lollipopkit/flutter_server_box PR 1026 — Modifies server edit/setting UI (including lib/view/page/server/edit/widget.dart) and touches GithubIds, overlapping at the same files.
  • lollipopkit/flutter_server_box PR 1076 — Updates the GithubIds.participants constant in lib/data/res/github_id.dart, directly related to the same constant change.
  • lollipopkit/flutter_server_box PR 1003 — (if applicable) Changes server import/deduplication utilities and may overlap with ServerDeduplication APIs and import flows.

Suggested reviewers

  • lollipopkit
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactoring: moving SSH import and discovery features from the server edit page to the settings page.
Linked Issues check ✅ Passed The PR addresses issue #950 by moving multi-server import/discovery controls from the single-server edit page to settings, improving desktop UI alignment and eliminating incorrect button display.
Out of Scope Changes check ✅ Passed All changes are in-scope: SSH feature relocation, simplified startup SSH config handling, desktop-only import toggle, and one contributor credit addition.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot requested a review from lollipopkit March 20, 2026 08:47
chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

… 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
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

…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
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

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
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

- 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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 the resolvedServers-only path.

When only resolvedServers is passed and it's empty, source.length equals 0, making the allExistMessage('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:

  1. Adding a doc comment clarifying that callers should pre-filter empty lists when passing only resolvedServers, or
  2. 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 addServer call (lines 96-98) performs individual persistence writes via Stores.server.put(spi) and Stores.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

📥 Commits

Reviewing files that changed from the base of the PR and between 28e9f65 and 0608d6a.

📒 Files selected for processing (4)
  • lib/core/utils/server_dedup.dart
  • lib/view/page/server/edit/actions.dart
  • lib/view/page/server/edit/edit.dart
  • lib/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

devin-ai-integration[bot]

This comment was marked as resolved.

…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
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…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
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
lib/view/page/setting/entries/ssh.dart (1)

130-136: ⚠️ Potential issue | 🟡 Minor

Localize 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 | 🟡 Minor

Clear firstTimeReadSSHCfg on 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 a finally, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 522f7c4 and c6b380b.

📒 Files selected for processing (3)
  • lib/core/utils/server_dedup.dart
  • lib/view/page/server/edit/actions.dart
  • lib/view/page/setting/entries/ssh.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/core/utils/server_dedup.dart

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 20, 2026
…SSH configuration access was denied

When SSH configuration access is denied, set the “first read” flag to false to prevent repeated prompts
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…mporting SSH configurations

Moved the logic for checking existing servers to an earlier stage to avoid unnecessary parsing of SSH configurations
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 12 additional findings in Devin Review.

Open in Devin Review

Comment on lines +142 to +145
final hasExistingServers = ref.read(serversProvider).servers.isNotEmpty;
if (hasExistingServers) {
Stores.setting.firstTimeReadSSHCfg.put(false);
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@GT-610 GT-610 merged commit 1bea565 into lollipopkit:main Mar 20, 2026
2 checks passed
@GT-610 GT-610 deleted the entry-move branch March 20, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

opt.: auto list styles on Desktop

1 participant