Skip to content

Conversation

@lollipopkit
Copy link
Owner

@lollipopkit lollipopkit commented Jan 14, 2026

Fixes #356

Summary by CodeRabbit

  • New Features

    • Multi-hop jump-server chains with per-hop key handling, iterative host-key verification, UI to build/view/edit chains, and confirmation when chains change
    • genClient and connection flows now accept/respect explicit hop chains and per-hop private keys; migration and persistence for chain-based settings
  • Bug Fixes

    • Improved jump-loop detection to prevent cycles
    • Corrected connection status label to "connected"
  • Tests

    • Added unit tests for chain merging, deduplication, and loop detection

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds multi‑hop jump‑server support across model, storage, UI and SSH core: new jumpChainIds/jumpChain fields, resolveMergedJumpChain, multi‑hop genClient/_genClientInternal with per‑hop host‑key checks, cycle detection, migration, UI editor, and tests.

Changes

Cohort / File(s) Summary
Core SSH client generation
lib/core/utils/server.dart
Added resolveMergedJumpChain and _resolveMergedJumpChainInternal; extended genClient to accept jumpChain/jumpPrivateKeys; added _genClientInternal; implemented multi‑hop SSH chaining, per‑hop host‑key checks, per‑hop private‑key resolution, visited‑set cycle detection, and cleanup logic.
SFTP request & worker
lib/data/model/sftp/req.dart, lib/data/model/sftp/worker.dart
Replaced single‑hop jumpSpi/jumpPrivateKey with jumpChain/jumpPrivateKeys; build chains from resolved IDs; updated upload/download flows; corrected enum to sshConnected.
Server model & serialization (freezed/g.dart)
lib/data/model/server/server_private_info.dart, lib/data/model/server/server_private_info.freezed.dart, lib/data/model/server/server_private_info.g.dart
Added jumpChainIds: List<String>? to Spi; migration from legacy jumpId; equality, copyWith, JSON (de)serialization updated to include new field.
Hive adapters & schema
lib/hive/hive_adapters.g.dart, lib/hive/hive_adapters.g.yaml
Added jumpChainIds to Hive Spi adapter (new field index; nextIndex incremented); read/write and constructor updated.
Storage & migration
lib/data/store/server.dart
migrateIds() updated to compute/remap jumpChainIds (from existing chain or legacy jumpId), persist new chain and clear legacy jumpId.
Providers & generated hashes
lib/data/provider/server/all.g.dart, lib/data/provider/server/single.dart, lib/data/provider/server/single.g.dart
Adjusted connected/jump logging to consider jumpChainIds; updated generated debug hash literals (no logic change).
Server edit UI (jump chain)
lib/view/page/server/edit/edit.dart, lib/view/page/server/edit/widget.dart, lib/view/page/server/edit/jump_chain.dart, lib/view/page/server/edit/actions.dart
Replaced single‑jump UI/state with _jumpChain; added _buildJumpChain() UI, dialogs to add/reorder/delete hops, cycle detection, user confirmation on chain change, and persistence of jumpChainIds.
Storage SFTP UI fix
lib/view/page/storage/sftp_mission.dart
Corrected enum match to SftpWorkerStatus.sshConnected.
Tests
test/jump_server_test.dart
Added unit tests for resolveMergedJumpChain: missing‑hop error, merge/dedupe, and loop detection.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant API as genClient
    participant Core as _genClientInternal
    participant Store as SPI Resolver
    participant JumpA as Jump Server A
    participant JumpB as Jump Server B
    participant Target as Target Server

    App->>API: genClient(targetSpi, jumpChain=[A,B], jumpPrivateKeys=[kA,kB])
    API->>Core: _genClientInternal(spi, jumpChain?, visited={})
    Core->>Store: resolveMergedJumpChain(targetSpi) -> [A,B]
    Core->>Core: validate visited & normalize IDs
    Core->>JumpA: establish SSH (private key kA) -> clientA
    Core->>JumpB: connect via clientA (private key kB) -> clientB
    Core->>Target: connect via clientB -> final SSH client
    Core-->>API: return final SSH client
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive Minor unrelated changes detected: typo fix (sshConnectted→sshConnected) and hash updates in generated files are outside the jump chain feature scope. Verify whether the typo fix and hash updates are acceptable as minor cleanup, or consider separating them into a dedicated housekeeping PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: jump server chain' clearly and concisely summarizes the main change: adding support for chained jump servers.
Linked Issues check ✅ Passed The PR implements multi-level jump server support by replacing single jumpSpi/jumpId fields with jumpChain/jumpChainIds, enabling traversal of multiple intermediate servers as requested in #356.
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
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lollipopkit/issue356

🧹 Recent nitpick comments
lib/core/utils/server.dart (1)

180-287: Resource cleanup is properly implemented.

The try-catch block (lines 225-284) correctly tracks created clients and cleans them up on error. The createdClients list is returned on success for lifecycle management. This addresses the previously flagged resource leak concern.

Minor code duplication: The injectedSpiMap building logic (lines 184-197) is nearly identical to lines 45-53 in resolveMergedJumpChain. Consider extracting to a shared helper to reduce duplication.

♻️ Optional: Extract shared map-building logic
// Add helper function
Map<String, Spi> _buildInjectedSpiMap(List<Spi>? jumpChain) {
  final map = <String, Spi>{};
  if (jumpChain == null) return map;
  for (final s in jumpChain) {
    map[s.id] = s;
    if (s.oldId.isNotEmpty) map[s.oldId] = s;
  }
  return map;
}

Then use in both resolveMergedJumpChain and _genClientInternal.


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52c8079 and 61f161d.

📒 Files selected for processing (1)
  • lib/core/utils/server.dart
🔇 Additional comments (6)
lib/core/utils/server.dart (6)

41-69: LGTM!

The resolveMergedJumpChain function properly handles both injected chain and store-based lookups, with correct guarding of empty oldId values (lines 49-50). The error handling for missing jump servers is clear and informative.


71-110: LGTM!

The DFS-based traversal correctly produces a topologically-ordered hop list with proper cycle detection. The dual-set approach (stack for current path, seen for deduplication) is a standard pattern for detecting cycles in directed graphs.


112-170: LGTM!

The public genClient API is cleanly separated from the internal _genClientInternal implementation. The visited set initialization at the public boundary and cycle detection at line 168-170 properly prevent infinite recursion in jump chains.


343-359: LGTM!

The hop client lifecycle is properly tied to the target client via client.done.whenComplete(). This ensures intermediate SSH connections are closed when the target client disconnects, addressing the previously flagged resource leak concern.


516-556: LGTM!

The ensureKnownHostKey function now properly:

  1. Traverses the full jump chain via resolveMergedJumpChain (addressing the P1 concern about ignoring jumpChainIds)
  2. Routes each hop's host-key verification through preceding hops (addressing the P2 concern about direct TCP connections to unreachable intermediates)

The implementation correctly builds preceding hop lists and keys for each verification step.


558-587: LGTM!

The helper correctly verifies host keys through the provided jump chain context. The reload at line 585 ensures newly-persisted fingerprints are available for subsequent hop verifications.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

@lollipopkit
Copy link
Owner Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8be9b9b10b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 46 to 47
for (final hopId in queue) {
final extra = flattenHopIds(hopId, visited: visited);

Choose a reason for hiding this comment

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

P2 Badge Fix cycle check to traverse nested jump chains

The new containsCycleWithCandidate only checks for direct duplicates and then calls flattenHopIds with a visited set that already contains every hop ID from the first loop, so flattenHopIds immediately returns without traversing each hop’s existing jumpChainIds. This means a candidate hop that already routes back to the current server (or through another hop) is still selectable, and the cycle won’t be caught until runtime in genClient (triggering the SSHErr loop detection). To actually prevent circular configurations in the UI, call flattenHopIds with a fresh visited (or add hops after the recursive walk) so nested chains are inspected.

Useful? React with 👍 / 👎.

Comment on lines 461 to 462
final jumpSpi = spi.jumpId != null ? Stores.server.box.get(spi.jumpId) : null;
if (jumpSpi != null && !_hasKnownHostFingerprintForSpi(jumpSpi, cache)) {

Choose a reason for hiding this comment

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

P1 Badge Ensure known host keys traverse jumpChainIds

ensureKnownHostKey still only walks spi.jumpId and ignores jumpChainIds, so in multi-hop setups only the legacy single hop is pre-accepted. This is problematic for flows that rely on pre-seeded fingerprints (e.g., SFTP in an isolate where the default host key prompt cannot show UI and returns false), because unknown host keys on intermediate hops will cause connection failure even though the target’s chain is configured. The function should iterate the full jumpChainIds list (and detect loops) to ensure all hop fingerprints are accepted before connecting.

Useful? React with 👍 / 👎.

Copy link

@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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/data/provider/server/single.dart (1)

138-142: Add defensive check for empty jumpChainIds list to align with other code paths.

The condition only checks for null, but other parts of the codebase defensively check for both null and empty list (e.g., lib/view/page/server/edit/jump_chain.dart:27 and lib/core/utils/server.dart:116). While the save flow normalizes empty lists to null, the model's copyWith does not enforce this, making an empty list theoretically possible.

Suggested change
-        if (spi.jumpChainIds == null && spi.jumpId == null) {
+        if ((spi.jumpChainIds?.isNotEmpty != true) && spi.jumpId == null) {
🤖 Fix all issues with AI agents
In `@lib/view/page/server/edit/actions.dart`:
- Around line 225-232: The confirmation dialog is shown for any edit when spi !=
null but should only trigger if the jump chain actually changed; update the
condition around the context.showRoundDialog call to compare the current
_jumpChain.value with the original spi.jumpChainIds (or equivalent stored
originalJumpChain) and only show the dialog when they differ, leaving the
existing dialog text (libL10n.askContinue('${l10n.jumpServer}
${libL10n.setting}')) and action handling untouched; alternatively, if you
prefer a generic confirmation, replace the message with a neutral string from
libL10n and keep the current spi != null condition.
🧹 Nitpick comments (2)
lib/data/model/server/server_private_info.dart (1)

102-110: Consider simplifying isSameAs to only check jumpChainIds post-migration.

The method checks both jumpId and jumpChainIds for equality. After migration completes, jumpId should be null and jumpChainIds should hold all hop data. Checking both fields may cause false negatives if one server was migrated and another wasn't, even though they represent the same jump configuration.

If backward compatibility with pre-migration data is required in isSameAs, this is fine. Otherwise, consider normalizing both sides before comparison or only comparing jumpChainIds.

lib/view/page/server/edit/jump_chain.dart (1)

118-119: Consider responsive width for the editor dialog.

The fixed width: 320 may not adapt well to different screen sizes (e.g., narrow phones or tablets). Consider using a responsive approach.

♻️ Optional: Use responsive constraints
           child: SizedBox(
-            width: 320,
+            width: MediaQuery.of(context).size.width * 0.8,
             child: _jumpChain.listenVal((hops) {

Or use ConstrainedBox with maxWidth for better flexibility.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c51cf62 and 8be9b9b.

📒 Files selected for processing (15)
  • lib/core/utils/server.dart
  • lib/data/model/server/server_private_info.dart
  • lib/data/model/server/server_private_info.freezed.dart
  • lib/data/model/server/server_private_info.g.dart
  • lib/data/model/sftp/req.dart
  • lib/data/provider/server/all.g.dart
  • lib/data/provider/server/single.dart
  • lib/data/provider/server/single.g.dart
  • lib/data/store/server.dart
  • lib/hive/hive_adapters.g.dart
  • lib/hive/hive_adapters.g.yaml
  • lib/view/page/server/edit/actions.dart
  • lib/view/page/server/edit/edit.dart
  • lib/view/page/server/edit/jump_chain.dart
  • lib/view/page/server/edit/widget.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/view/page/server/edit/widget.dart
  • lib/core/utils/server.dart
🧰 Additional context used
📓 Path-based instructions (7)
lib/data/provider/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Riverpod providers by feature under lib/data/provider/

Files:

  • lib/data/provider/server/single.dart
  • lib/data/provider/server/single.g.dart
  • lib/data/provider/server/all.g.dart
**/*.{g,freezed}.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Do not manually edit generated Dart files (*.g.dart, *.freezed.dart)

Files:

  • lib/data/model/server/server_private_info.g.dart
  • lib/data/provider/server/single.g.dart
  • lib/data/model/server/server_private_info.freezed.dart
  • lib/hive/hive_adapters.g.dart
  • lib/data/provider/server/all.g.dart
lib/data/model/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

After modifying annotated model files, run: dart run build_runner build --delete-conflicting-outputs

Files:

  • lib/data/model/server/server_private_info.g.dart
  • lib/data/model/sftp/req.dart
  • lib/data/model/server/server_private_info.freezed.dart
  • lib/data/model/server/server_private_info.dart
lib/data/model/server/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Place server management models under lib/data/model/server/

Files:

  • lib/data/model/server/server_private_info.g.dart
  • lib/data/model/server/server_private_info.freezed.dart
  • lib/data/model/server/server_private_info.dart
lib/data/store/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Persist state using Hive stores placed under lib/data/store/

Files:

  • lib/data/store/server.dart
lib/view/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/view/**/*.dart: Prefer widgets/utilities from fl_lib for common UI (e.g., CustomAppBar, context.showRoundDialog, Input, Btnx.cancelOk)
Prefer using libL10n strings before adding new ones to project l10n
Split UI into build, actions, and utils; use extension on to separate concerns

Files:

  • lib/view/page/server/edit/jump_chain.dart
  • lib/view/page/server/edit/edit.dart
  • lib/view/page/server/edit/actions.dart
lib/data/model/sftp/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Place SFTP models under lib/data/model/sftp/

Files:

  • lib/data/model/sftp/req.dart
🧠 Learnings (8)
📚 Learning: 2026-01-11T10:26:03.064Z
Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 999
File: lib/data/provider/server/all.dart:0-0
Timestamp: 2026-01-11T10:26:03.064Z
Learning: In lib/data/provider/server/all.dart, the `_refreshInProgress` guard was removed because: (1) each server has its own `_isRefreshing` protection in single.dart that prevents concurrent refreshes per server, (2) the coordination overhead in all.dart is minimal (milliseconds) with no harm from concurrent calls, (3) no code awaits the completer, and (4) removing it enables true parallelism where servers refresh independently without artificial serialization at the coordination layer.

Applied to files:

  • lib/data/provider/server/single.dart
  • lib/data/provider/server/single.g.dart
  • lib/data/provider/server/all.g.dart
📚 Learning: 2026-01-11T05:42:59.995Z
Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 0
File: :0-0
Timestamp: 2026-01-11T05:42:59.995Z
Learning: In lib/data/provider/server/single.dart, the `_updateServer()` method is kept as a separate method (rather than inlined) for abstraction and future extensibility, clearly separating the refresh lock logic from the actual data fetching operation.

Applied to files:

  • lib/data/provider/server/single.dart
  • lib/data/provider/server/single.g.dart
  • lib/data/provider/server/all.g.dart
  • lib/data/model/server/server_private_info.dart
📚 Learning: 2026-01-11T05:42:59.995Z
Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 0
File: :0-0
Timestamp: 2026-01-11T05:42:59.995Z
Learning: In lib/data/provider/server/single.dart, the Boolean flag `_isRefreshing` is used intentionally for concurrency control instead of a Completer pattern. This is because all.dart uses `Future.wait(refreshFutures)` to coordinate parallel refreshes across multiple independent server notifiers, and the Boolean flag provides per-server concurrency protection while maintaining parallelism across servers.

Applied to files:

  • lib/data/provider/server/single.g.dart
  • lib/data/provider/server/all.g.dart
  • lib/data/model/server/server_private_info.dart
📚 Learning: 2025-10-08T09:36:41.682Z
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/data/model/server/**/*.dart : Place server management models under lib/data/model/server/

Applied to files:

  • lib/data/store/server.dart
  • lib/data/provider/server/all.g.dart
  • lib/data/model/server/server_private_info.dart
📚 Learning: 2025-10-08T09:36:41.682Z
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/data/model/sftp/**/*.dart : Place SFTP models under lib/data/model/sftp/

Applied to files:

  • lib/data/model/sftp/req.dart
  • lib/data/model/server/server_private_info.dart
📚 Learning: 2025-10-08T09:36:41.682Z
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/data/model/ssh/**/*.dart : Place SSH models under lib/data/model/ssh/

Applied to files:

  • lib/data/model/sftp/req.dart
📚 Learning: 2025-10-08T09:36:41.682Z
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/view/**/*.dart : Prefer widgets/utilities from fl_lib for common UI (e.g., CustomAppBar, context.showRoundDialog, Input, Btnx.cancelOk)

Applied to files:

  • lib/data/model/server/server_private_info.dart
📚 Learning: 2025-10-08T09:36:41.682Z
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/view/**/*.dart : Prefer using libL10n strings before adding new ones to project l10n

Applied to files:

  • lib/data/model/server/server_private_info.dart
🔇 Additional comments (17)
lib/data/provider/server/all.g.dart (1)

44-44: Generated hash update looks correct.

This hash change reflects the updated ServersNotifier implementation for the jump chain feature. As per coding guidelines, this generated file should not be manually edited.

lib/data/provider/server/single.g.dart (1)

61-61: Generated hash update looks correct.

This hash change reflects the updated ServerNotifier implementation. As per coding guidelines, this generated file should not be manually edited.

lib/hive/hive_adapters.g.yaml (1)

28-65: Schema changes for jump chain feature look correct.

The jumpChainIds field is added with the correct index (16), and nextIndex is properly incremented to 17. Retaining the legacy jumpId field (index 9) ensures backward compatibility for existing data during migration.

lib/view/page/server/edit/edit.dart (3)

28-29: Part file addition for jump chain UI looks good.

The modular approach of separating jump chain UI into its own part file follows the coding guidelines for organizing view layer code.


70-70: State migration from single hop to chain-based list is correct.

The .vn extension properly creates a ValueNotifier<List<String>> for the jump chain state.


104-104: Jump chain UI integration is already in place.

The _buildJumpChain() is properly integrated in the widget part file (widget.dart, line 135) and the disposal at line 104 is correctly updated. No action required.

lib/data/model/server/server_private_info.dart (2)

39-47: Well-documented field addition with clear migration path.

The documentation clearly explains the relationship between legacy jumpId and the new jumpChainIds, making the migration strategy transparent for future maintainers.


90-93: Migration logic correctly promotes jumpId to jumpChainIds.

The fallback jumpChainIds ?? (jumpId == null ? null : [jumpId!]) properly handles legacy single-hop configurations by wrapping them in a list.

lib/data/store/server.dart (1)

90-98: Jump chain migration logic is correct.

The migration properly:

  1. Derives the chain from jumpChainIds or falls back to wrapping jumpId
  2. Remaps IDs through idMap while preserving unmapped IDs
  3. Clears the legacy jumpId field after migration

The fallback idMap[e] ?? e ensures that jump targets referencing servers that weren't migrated (already had new-format IDs) are preserved.

lib/view/page/server/edit/actions.dart (2)

289-290: Correct transition to chain-based jump configuration.

Setting jumpId: null and populating jumpChainIds ensures new/updated servers use the chain model exclusively.


434-434: Initialization correctly handles both legacy and new formats.

The fallback logic mirrors the migration pattern used elsewhere, ensuring consistent behavior when loading existing server configurations.

lib/data/model/sftp/req.dart (2)

18-43: Jump chain construction with loop detection looks correct.

The loop detection using a visited set is well-implemented. One observation: on line 26, if a hop server is not found (hopSpi == null), the loop silently breaks, resulting in a partial chain. This may be intentional (graceful degradation), but consider logging a warning or throwing an error if a configured hop is missing, as it could indicate data corruption or misconfiguration.

💡 Optional: Add warning for missing hop
         final hopSpi = Stores.server.box.get(hopId);
-        if (hopSpi == null) break;
+        if (hopSpi == null) {
+          Loggers.app.warning('Jump hop server not found: $hopId');
+          break;
+        }

115-115: Good typo fix.

Correcting sshConnecttedsshConnected improves code clarity.

lib/view/page/server/edit/jump_chain.dart (3)

21-35: Recursive flattening with cycle protection is well-implemented.

The flattenHopIds function correctly uses a visited set to prevent infinite recursion when traversing nested jump chains. The early return on cycle detection (line 22) is appropriate.


37-53: Cycle detection for candidates is thorough.

The two-phase approach correctly validates both direct cycles (candidate equals self or duplicates in chain) and transitive cycles (through nested jump chains). The shared visited set ensures consistent tracking across both phases.


92-113: Clear UX flow for adding and managing hops.

The pattern of adding a hop first (line 113), then opening the editor dialog for further management is intuitive. Users can quickly add a single hop or continue to reorder/remove as needed.

lib/data/model/server/server_private_info.freezed.dart (1)

1-10: Generated file — no manual review required.

This is auto-generated by Freezed. Per coding guidelines, do not manually edit *.freezed.dart files. After modifying the annotated model in server_private_info.dart, ensure the code is regenerated with:

dart run build_runner build --delete-conflicting-outputs

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/core/utils/server.dart`:
- Around line 240-264: Wrap the hop/chain-building loop in a try-catch-finally
and maintain a List<SSHClient> (e.g., named createdClients) that you append each
currentClient returned from _genClientInternal (and the initial client) to; if
any await (forwardLocal or _genClientInternal) throws, iterate createdClients
and call close() on each to avoid leaks, rethrow the error, and on success close
any intermediate clients that are no longer needed before returning the final
forwarded socket; ensure you reference currentClient, _genClientInternal,
forwardLocal and resolveHopPrivateKey when updating the code so intermediate
SSHClient instances are properly closed in both error and success paths.
🧹 Nitpick comments (5)
lib/view/page/server/edit/actions.dart (1)

225-247: Jump chain change detection looks correct.

The logic properly addresses the previous review concern by only showing the confirmation dialog when the jump chain actually changes. The comparison handles empty lists, length differences, and element-by-element checks.

Consider using listEquals from package:flutter/foundation.dart for cleaner comparison:

♻️ Optional simplification
+import 'package:flutter/foundation.dart' show listEquals;
+
 final oldSpi = this.spi;
 if (oldSpi != null) {
   final originalJumpChain = oldSpi.jumpChainIds ?? (oldSpi.jumpId == null ? const <String>[] : [oldSpi.jumpId!]);
   final currentJumpChain = _jumpChain.value;

-  final jumpChainChanged = () {
-    if (originalJumpChain.isEmpty && currentJumpChain.isEmpty) return false;
-    if (originalJumpChain.length != currentJumpChain.length) return true;
-    for (var i = 0; i < originalJumpChain.length; i++) {
-      if (originalJumpChain[i] != currentJumpChain[i]) return true;
-    }
-    return false;
-  }();
+  final jumpChainChanged = !listEquals(originalJumpChain, currentJumpChain);

   if (jumpChainChanged) {
lib/view/page/server/edit/jump_chain.dart (3)

55-66: Potential duplicate display if chain contains repeated hops.

If _jumpChain.value contains the same hop ID twice (e.g., through manual data manipulation), buildTextNearToFar would add it to flat twice but only traverse its nested chain once due to the visited set. The UI should prevent duplicates via containsCycleWithCandidate, but this edge case could cause confusing display text.

Consider adding deduplication or a defensive check:

♻️ Optional defensive check
 String? buildTextNearToFar() {
   if (_jumpChain.value.isEmpty) return null;
   final flat = <String>[];
   final visited = <String>{selfId};
   for (final hopId in _jumpChain.value) {
+    if (visited.contains(hopId)) continue; // skip duplicates
     flat.add(hopId);
     flat.addAll(flattenHopIds(hopId, visited: visited));
   }

121-167: Consider using ReorderableListView for better UX.

The manual up/down arrow buttons work but Flutter's ReorderableListView provides a more intuitive drag-and-drop experience for reordering items. This would reduce button clutter and improve touch accessibility.

♻️ Suggested alternative with ReorderableListView
ReorderableListView.builder(
  shrinkWrap: true,
  itemCount: hops.length,
  onReorder: (oldIndex, newIndex) {
    if (newIndex > oldIndex) newIndex--;
    final list = [..._jumpChain.value];
    final item = list.removeAt(oldIndex);
    list.insert(newIndex, item);
    _jumpChain.value = list;
  },
  itemBuilder: (context, index) {
    final id = hops[index];
    return ListTile(
      key: ValueKey(id),
      title: Text(serverNameOrId(id)),
      subtitle: Text(id, style: UIs.textGrey),
      trailing: IconButton(
        icon: const Icon(Icons.delete, size: 18),
        onPressed: () {
          final list = [..._jumpChain.value]..removeAt(index);
          _jumpChain.value = list;
        },
      ),
    );
  },
)

86-90: Clarify the distinction between "Route" and "Path" labels.

The UI shows both l10n.route (near-to-far) and libL10n.path (far-to-near), but both display the same chain in opposite orders. Users may find this confusing without context about why both are shown.

Consider either:

  1. Adding tooltips or help text explaining the difference
  2. Showing only one representation (typically the connection order: far-to-near)
  3. Using clearer labels like "Connection order" vs "Return path"
lib/core/utils/server.dart (1)

182-215: Duplicated resolveSpi logic.

The resolveSpi function at lines 197-208 duplicates the logic from lines 53-64 in resolveMergedJumpChain. Consider extracting a shared helper to reduce duplication and ensure consistent behavior.

♻️ Suggested extraction
Spi Function(String) _buildSpiResolver({
  required Map<String, Spi> injectedSpiMap,
  required bool strictMode, // true when jumpChain != null
}) {
  return (String id) {
    final injected = injectedSpiMap[id];
    if (injected != null) return injected;
    if (strictMode) {
      throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
    }
    final fromStore = Stores.server.box.get(id);
    if (fromStore == null) {
      throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
    }
    return fromStore;
  };
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8be9b9b and 7d30af4.

📒 Files selected for processing (4)
  • lib/core/utils/server.dart
  • lib/view/page/server/edit/actions.dart
  • lib/view/page/server/edit/jump_chain.dart
  • test/jump_server_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/jump_server_test.dart
🧰 Additional context used
📓 Path-based instructions (1)
lib/view/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/view/**/*.dart: Prefer widgets/utilities from fl_lib for common UI (e.g., CustomAppBar, context.showRoundDialog, Input, Btnx.cancelOk)
Prefer using libL10n strings before adding new ones to project l10n
Split UI into build, actions, and utils; use extension on to separate concerns

Files:

  • lib/view/page/server/edit/jump_chain.dart
  • lib/view/page/server/edit/actions.dart
🔇 Additional comments (7)
lib/view/page/server/edit/actions.dart (2)

304-305: Migration from single jumpId to jumpChainIds is handled correctly.

Setting jumpId: null and populating jumpChainIds aligns with the new chain-based model. The conditional null assignment when the chain is empty avoids storing unnecessary empty lists.


449-449: Initialization properly handles backward compatibility.

The fallback logic spi.jumpChainIds ?? (spi.jumpId == null ? const <String>[] : [spi.jumpId!]) correctly migrates existing servers with single jumpId to the new chain representation.

lib/view/page/server/edit/jump_chain.dart (2)

9-15: Good guard for new server creation.

When selfId is null (creating a new server), showing a placeholder tile is appropriate since jump chains can only reference existing servers. The user must save the server first before configuring jump chains.


37-53: Cycle detection at UI level is limited to cycles involving the current server; transitive cycles among other servers are only detected at runtime.

The implementation checks for:

  1. Direct duplicates in the proposed chain (line 43)
  2. Paths that loop back to selfId (lines 46-51)

However, it doesn't prevent cycles formed entirely among jump servers that don't include selfId. For example, if server A's chain includes B and B's chain includes A, adding either as a candidate would pass this check when selfId is neither A nor B.

This is mitigated by runtime detection in genOrderedJumpChain() which uses DFS with stack-based cycle detection and catches all cycles regardless of whether they involve selfId. The error will be visible to the user as a connection failure. If immediate UI-level feedback is important for usability, the UI check could be enhanced to use the same graph-wide detection, but the current approach is acceptable given the runtime protection.

lib/core/utils/server.dart (3)

69-108: LGTM - Well-structured DFS with proper cycle detection.

The jump chain resolution correctly handles both jumpChainIds and legacy jumpId fallback, with proper cycle detection using separate seen and stack sets. The topological ordering ensures dependencies are resolved before dependents.

Minor note: Line 98's seen.add(norm) in the condition is slightly redundant since line 89 already guards against re-entry, but this is a harmless defensive pattern.


110-148: LGTM - Clean public API with internal delegation.

The updated signature correctly introduces jumpChain and jumpPrivateKeys for isolate contexts while maintaining backward compatibility. Initializing visited at the public entry point ensures cycle detection across the entire connection tree.


478-501: Addresses past review concern: now traverses full jump chain for host key verification.

The updated implementation correctly uses resolveMergedJumpChain to iterate all hops and verify their host keys before connecting to the target. This resolves the previously flagged issue where only spi.jumpId was checked.

Note: _ensureKnownHostKeyForSingle calls genClient without passing jumpChain/jumpPrivateKeys, so this function will not work correctly in isolate contexts where Stores is unavailable. If isolate support is needed for host key pre-verification, consider adding these parameters to ensureKnownHostKey.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@lollipopkit
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@lollipopkit
Copy link
Owner Author

@coderabbitai Why still blocking?

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check the current status of PR `#1006`
echo "=== PR Status ==="
gh pr view 1006 --json state,isDraft,mergeable,mergeStateStatus,statusCheckRollup,reviewDecision

echo -e "\n=== PR Checks ==="
gh pr checks 1006

echo -e "\n=== Review Comments ==="
gh pr view 1006 --json comments --jq '.comments[] | "Comment by \(.author.login): \(.body[0:200])"'

echo -e "\n=== Review Threads ==="
gh api repos/:owner/:repo/pulls/1006/reviews --jq '.[] | "Review by \(.user.login): \(.state)"'

Length of output: 1567


🏁 Script executed:

#!/bin/bash

# Get detailed review comments from CodeRabbit
echo "=== CodeRabbit Review Comments ==="
gh api repos/:owner/:repo/pulls/1006/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {path: .path, line: .line, body: .body[0:500]}'

echo -e "\n=== Review Summary ==="
gh pr view 1006 --json reviews --jq '.reviews[] | select(.author.login == "coderabbitai[bot]") | {state: .state, submitted_at: .submittedAt, body: .body[0:300]}'

Length of output: 1260


@lollipopkit The PR is currently blocked because there are unresolved review comments requesting changes. While all CI checks are passing ✅ and the PR is technically mergeable, the review status shows "CHANGES_REQUESTED" from CodeRabbit.

Here are the pending review comments that need attention:

  1. 🟡 Minor issue in lib/view/page/server/edit/actions.dart:

    • The confirmation dialog triggers for all edits to an existing server, not just when the jump chain changes
    • This may confuse users editing unrelated fields like password or port
  2. 🟠 Major issue in lib/core/utils/server.dart:

    • There's a review comment on this file (details in the PR review thread)

To unblock the PR, you can:

  • Address the review comments by making the suggested changes
  • Reply to the review comments if you believe they're not applicable or already handled
  • If you've addressed them, I can review again or you can request me to approve the PR

Would you like me to take another look at the specific issues, or would you prefer to address them first?

Copy link

@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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/data/model/sftp/req.dart`:
- Around line 26-29: Replace the silent truncation when a hop is missing:
instead of calling Loggers.app.warning('Jump server not found: $hopId,
truncating chain') and break, throw an SSHErr (the same error type used in
server.dart) including a clear message with hopId so callers can handle the
failure consistently; update the surrounding code paths that build/validate the
hop chain to propagate or catch this SSHErr as appropriate.
♻️ Duplicate comments (1)
lib/core/utils/server.dart (1)

222-286: Resource leak on success path persists.

The error-path cleanup (lines 273-282) is now implemented, but the fundamental issue remains: on success, createdClients goes out of scope and the intermediate SSHClient instances become unreachable while still holding resources.

The comment at lines 284-285 correctly explains the constraint—the socket tunnels through these clients, so they must remain open. However, without returning or tracking these clients, they can never be properly closed when the caller is finished.

Consider returning a wrapper that includes both the socket and the intermediate clients, enabling the caller to manage their lifecycle:

class TunneledConnection {
  final SSHSocket socket;
  final List<SSHClient> _intermediateClients;
  
  void close() {
    for (final client in _intermediateClients) {
      client.close();
    }
  }
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 928f2be and 827d40b.

📒 Files selected for processing (2)
  • lib/core/utils/server.dart
  • lib/data/model/sftp/req.dart
🧰 Additional context used
📓 Path-based instructions (2)
lib/data/model/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

After modifying annotated model files, run: dart run build_runner build --delete-conflicting-outputs

Files:

  • lib/data/model/sftp/req.dart
lib/data/model/sftp/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Place SFTP models under lib/data/model/sftp/

Files:

  • lib/data/model/sftp/req.dart
🧠 Learnings (2)
📚 Learning: 2025-10-08T09:36:41.682Z
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/data/model/sftp/**/*.dart : Place SFTP models under lib/data/model/sftp/

Applied to files:

  • lib/data/model/sftp/req.dart
📚 Learning: 2025-10-08T09:36:41.682Z
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/data/model/ssh/**/*.dart : Place SSH models under lib/data/model/ssh/

Applied to files:

  • lib/data/model/sftp/req.dart
🔇 Additional comments (7)
lib/data/model/sftp/req.dart (3)

9-10: LGTM! Clean replacement of single-hop fields with chain-based structure.

The new jumpChain and jumpPrivateKeys fields properly support multi-hop traversal while maintaining alignment by index.


30-36: LGTM! Loop detection logic is correct.

The visited set properly tracks both the target server and all processed hops, throwing SSHErr when a cycle is detected.


117-117: LGTM! Good typo fix.

Correcting sshConnecttedsshConnected improves API consistency.

lib/core/utils/server.dart (4)

41-110: LGTM! Well-implemented DFS-based chain resolution with cycle detection.

The algorithm correctly:

  • Uses stack for detecting cycles during DFS traversal
  • Uses seen for deduplication of fully processed nodes
  • Produces topologically-sorted output (dependencies before dependents)
  • Handles both jumpChainIds and legacy jumpId fallback

112-150: LGTM! Clean public API extension.

The public genClient maintains backward compatibility while properly initializing the visited set for cycle detection at the entry point.


499-522: LGTM! ensureKnownHostKey now properly traverses full jump chain.

This addresses the previous review concern—the function now uses resolveMergedJumpChain(spi) to resolve all hops (including jumpChainIds), ensuring all intermediate host keys are verified before connecting.


524-549: LGTM! Clean helper for per-hop host key verification.

The early return optimization (lines 530-532) avoids unnecessary connections, and the cache accumulation pattern (line 547) ensures newly accepted fingerprints are available for subsequent hops.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 26 to 29
if (hopSpi == null) {
Loggers.app.warning('Jump server not found: $hopId, truncating chain');
break;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: silent truncation vs throwing in server.dart.

When a hop is not found, this code logs a warning and truncates the chain, while server.dart throws SSHErr. This inconsistency could cause confusing failures—if a user configures chain [A, B, C] and B is missing, the chain silently becomes [A], leading to unexpected connection behavior.

Consider throwing SSHErr here for consistency:

Suggested fix
         if (hopSpi == null) {
-          Loggers.app.warning('Jump server not found: $hopId, truncating chain');
-          break;
+          throw SSHErr(
+            type: SSHErrType.connect,
+            message: 'Jump server not found: $hopId',
+          );
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (hopSpi == null) {
Loggers.app.warning('Jump server not found: $hopId, truncating chain');
break;
}
if (hopSpi == null) {
throw SSHErr(
type: SSHErrType.connect,
message: 'Jump server not found: $hopId',
);
}
🤖 Prompt for AI Agents
In `@lib/data/model/sftp/req.dart` around lines 26 - 29, Replace the silent
truncation when a hop is missing: instead of calling Loggers.app.warning('Jump
server not found: $hopId, truncating chain') and break, throw an SSHErr (the
same error type used in server.dart) including a clear message with hopId so
callers can handle the failure consistently; update the surrounding code paths
that build/validate the hop chain to propagate or catch this SSHErr as
appropriate.

@lollipopkit
Copy link
Owner Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6738ac94f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 23 to 27
final hopIds = spi.jumpChainIds ?? (spi.jumpId == null ? const <String>[] : [spi.jumpId!]);
for (final hopId in hopIds) {
final hopSpi = Stores.server.box.get(hopId);
if (hopSpi == null) {
throw SSHErr(

Choose a reason for hiding this comment

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

P1 Badge Include nested hop chains when building SFTP jumpChain

When an SFTP download/upload runs in the isolate, genClient will resolve the full merged jump chain and, if jumpChain is provided, will throw when a referenced hop is missing. Here the request only collects the immediate jumpChainIds/jumpId from the target and never expands hops that themselves have jumpChainIds, so any multi-hop configuration like “target -> A -> B” (where A has its own jump chain) will fail with “Jump server not found in provided chain” during isolate connections. This breaks SFTP for nested jump chains; consider using resolveMergedJumpChain (and aligning keys) or recursively collecting all hop dependencies.

Useful? React with 👍 / 👎.

@lollipopkit
Copy link
Owner Author

@codex review

@lollipopkit
Copy link
Owner Author

@coderabbitai Major issue in lib/core/utils/server.dart already has been solved, re-check it

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@lollipopkit I'll check the current state of lib/core/utils/server.dart to verify if the major issue has been resolved.


🏁 Script executed:

#!/bin/bash

# Fetch CodeRabbit's review comments on lib/core/utils/server.dart
echo "=== CodeRabbit Review Comments on lib/core/utils/server.dart ==="
gh api repos/:owner/:repo/pulls/1006/comments --jq '.[] | select(.user.login == "coderabbitai[bot]" and .path == "lib/core/utils/server.dart") | {line: .line, body: .body, created_at: .created_at, in_reply_to_id: .in_reply_to_id}'

echo -e "\n=== Current content of lib/core/utils/server.dart ==="
cat lib/core/utils/server.dart

Length of output: 26505


@lollipopkit You're absolutely right! ✅ The major issue in lib/core/utils/server.dart has been successfully resolved.

I can confirm that the current implementation (lines 228-281) now properly handles resource cleanup:

  1. Tracks all intermediate clients: A createdClients list is maintained to track all SSH clients created during the chain-building process
  2. Wraps in try-catch: The entire chain-building logic is wrapped in a try-catch block
  3. Cleanup on error: In the catch block (lines 272-280), all created clients are explicitly closed to prevent resource leaks
  4. Proper success handling: The comment on line 281 correctly explains why intermediate clients must remain open on success (the returned socket tunnels through them)

This implementation matches exactly what was suggested in the review and properly addresses the resource leak concern.


Remaining item:

There's still one minor issue pending in lib/view/page/server/edit/actions.dart regarding the confirmation dialog triggering for all edits rather than only when the jump chain changes. Would you like to address that as well, or would you prefer to handle it separately?


🧠 Learnings used
Learnt from: CR
Repo: lollipopkit/flutter_server_box PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-08T09:36:41.682Z
Learning: Applies to lib/data/model/server/**/*.dart : Place server management models under lib/data/model/server/

Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 999
File: lib/data/provider/server/all.dart:0-0
Timestamp: 2026-01-11T10:26:03.064Z
Learning: In lib/data/provider/server/all.dart, the `_refreshInProgress` guard was removed because: (1) each server has its own `_isRefreshing` protection in single.dart that prevents concurrent refreshes per server, (2) the coordination overhead in all.dart is minimal (milliseconds) with no harm from concurrent calls, (3) no code awaits the completer, and (4) removing it enables true parallelism where servers refresh independently without artificial serialization at the coordination layer.

@lollipopkit
Copy link
Owner Author

@coderabbitai Describe the minor issue

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Read the actions.dart file to examine the confirmation dialog logic
echo "=== Reading lib/view/page/server/edit/actions.dart ==="
cat lib/view/page/server/edit/actions.dart

Length of output: 16497


@lollipopkit Let me describe the minor issue that was originally flagged:

🟡 Original Issue: Jump Chain Confirmation Dialog

Location: lib/view/page/server/edit/actions.dart (lines 208-227)

The Concern:
The concern was that when editing an existing server, a confirmation dialog would appear asking users to confirm changes to jump server settings even when they only edited unrelated fields (like password, port, tags, etc.). This would create confusion because users who never touched the jump chain would still see a dialog about jump server settings.

Expected Behavior:

  • Dialog should only appear when the jump chain actually changes
  • Editing other fields (password, port, name, etc.) should not trigger this dialog

✅ Current Implementation Review

However, looking at the current code (lines 208-227), I can see that you've actually implemented proper checking:

final jumpChainChanged = () {
  if (originalJumpChain.isEmpty && currentJumpChain.isEmpty) return false;
  if (originalJumpChain.length != currentJumpChain.length) return true;
  for (var i = 0; i < originalJumpChain.length; i++) {
    if (originalJumpChain[i] != currentJumpChain[i]) return true;
  }
  return false;
}();

if (jumpChainChanged) {
  // Only show dialog when jump chain actually changed
  final ok = await context.showRoundDialog<bool>(...)
}

This logic correctly:

  1. ✅ Compares the original jump chain with the current one
  2. ✅ Only shows the dialog when jumpChainChanged is true
  3. ✅ Handles empty chains properly
  4. ✅ Checks both length and individual elements

It appears this issue has already been properly addressed in the current implementation! The dialog will only trigger when the jump chain is actually modified, not for unrelated edits.


Would you like me to verify this is working as expected, or can I mark this issue as resolved? 🤔


🧠 Learnings used
Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 999
File: lib/data/provider/server/all.dart:0-0
Timestamp: 2026-01-11T10:26:03.064Z
Learning: In lib/data/provider/server/all.dart, the `_refreshInProgress` guard was removed because: (1) each server has its own `_isRefreshing` protection in single.dart that prevents concurrent refreshes per server, (2) the coordination overhead in all.dart is minimal (milliseconds) with no harm from concurrent calls, (3) no code awaits the completer, and (4) removing it enables true parallelism where servers refresh independently without artificial serialization at the coordination layer.

Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 0
File: :0-0
Timestamp: 2026-01-11T06:20:59.076Z
Learning: In lib/view/page/server/detail/view.dart, the settings `cpuViewAsProgress`, `moveServerFuncs`, and `displayCpuIndex` are UI/display preferences that are modified through a separate settings page and cannot be changed while viewing the server detail page. Therefore, caching them as `late final` is intentional and acceptable—the values automatically refresh when users navigate away and back to the detail page, reducing redundant `fetch()` calls without compromising user experience.
</add_learning>

<!-- <review_comment_addressed>

Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 0
File: :0-0
Timestamp: 2026-01-11T05:42:59.995Z
Learning: In lib/data/provider/server/single.dart, the Boolean flag `_isRefreshing` is used intentionally for concurrency control instead of a Completer pattern. This is because all.dart uses `Future.wait(refreshFutures)` to coordinate parallel refreshes across multiple independent server notifiers, and the Boolean flag provides per-server concurrency protection while maintaining parallelism across servers.

Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 0
File: :0-0
Timestamp: 2026-01-11T05:42:59.995Z
Learning: In lib/data/provider/server/single.dart, the `_updateServer()` method is kept as a separate method (rather than inlined) for abstraction and future extensibility, clearly separating the refresh lock logic from the actual data fetching operation.

@lollipopkit
Copy link
Owner Author

@coderabbitai Why still blocking?

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check current PR status including review decisions
echo "=== PR Status and Review Decision ==="
gh pr view 1006 --json state,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup

echo -e "\n=== All Reviews on PR ==="
gh api repos/:owner/:repo/pulls/1006/reviews --jq '.[] | {user: .user.login, state: .state, submitted_at: .submitted_at}'

echo -e "\n=== Unresolved Review Comment Threads ==="
gh api graphql -f query='
query {
  repository(owner: "lollipopkit", name: "flutter_server_box") {
    pullRequest(number: 1006) {
      reviewThreads(first: 50) {
        nodes {
          isResolved
          comments(first: 10) {
            nodes {
              author {
                login
              }
              body
              path
              createdAt
            }
          }
        }
      }
    }
  }
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {isResolved, firstCommentAuthor: .comments.nodes[0].author.login, path: .comments.nodes[0].path, body: .comments.nodes[0].body[0:200]}'

echo -e "\n=== CI Checks Status ==="
gh pr checks 1006

Length of output: 3150


@lollipopkit The PR is currently blocked by two separate issues:

1. ❌ Failing CI Check

The flutter analysis check is failing:

  • Check name: check
  • Status: FAILURE
  • Duration: 1m44s
  • Details

This needs to be fixed before the PR can be merged.

2. 📝 Unresolved Review Comment Threads

There are 5 unresolved review comment threads keeping the review status at CHANGES_REQUESTED:

From CodeRabbit (me):

  1. 🟠 Major - lib/core/utils/server.dart (lines 240-264) - This appears to be the resource cleanup issue you mentioned was fixed. I can resolve this thread if you confirm.
  2. 🟡 Minor - lib/data/model/sftp/req.dart - Inconsistent error handling (silent truncation vs throwing)

From Codex:

  1. 🟠 P1 - lib/core/utils/server.dart - ensureKnownHostKey still only walks spi.jumpId and ignores jumpChainIds
  2. 🟡 P2 - lib/view/page/server/edit/jump_chain.dart - Cycle check only checks direct duplicates, doesn't traverse nested jump chains
  3. 🟠 P1 - lib/data/model/sftp/req.dart - SFTP jumpChain doesn't include nested hop chains when running in isolate

✅ To Unblock:

  1. Fix the failing CI check (flutter analysis)
  2. Address or resolve the unresolved comment threads:
    • I can resolve my thread on server.dart since you've confirmed the resource cleanup is fixed
    • The other threads from Codex need to be addressed or discussed

Would you like me to resolve my comment thread on lib/core/utils/server.dart since we confirmed the fix, or would you prefer to address the other issues first?


🧠 Learnings used
Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 0
File: :0-0
Timestamp: 2026-01-11T06:20:59.076Z
Learning: In lib/view/page/server/detail/view.dart, the settings `cpuViewAsProgress`, `moveServerFuncs`, and `displayCpuIndex` are UI/display preferences that are modified through a separate settings page and cannot be changed while viewing the server detail page. Therefore, caching them as `late final` is intentional and acceptable—the values automatically refresh when users navigate away and back to the detail page, reducing redundant `fetch()` calls without compromising user experience.
</add_learning>

<!-- <review_comment_addressed>

Learnt from: GT-610
Repo: lollipopkit/flutter_server_box PR: 999
File: lib/data/provider/server/all.dart:0-0
Timestamp: 2026-01-11T10:26:03.064Z
Learning: In lib/data/provider/server/all.dart, the `_refreshInProgress` guard was removed because: (1) each server has its own `_isRefreshing` protection in single.dart that prevents concurrent refreshes per server, (2) the coordination overhead in all.dart is minimal (milliseconds) with no harm from concurrent calls, (3) no code awaits the completer, and (4) removing it enables true parallelism where servers refresh independently without artificial serialization at the coordination layer.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2eeb55c1d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 506 to 509
final hops = resolveMergedJumpChain(spi);
for (final hop in hops) {
cache = await _ensureKnownHostKeyForSingle(
hop,

Choose a reason for hiding this comment

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

P2 Badge Route host-key checks through prior hops

Here ensureKnownHostKey iterates the merged hop list and calls _ensureKnownHostKeyForSingle for each hop, but that helper uses genClient(hop, ...) with no jump-chain context. In a multi-hop setup where a hop is only reachable through earlier hops (e.g., chain A→B and B has no jumpChainIds of its own), this will attempt a direct TCP connection to B, time out, and the SFTP host‑key precheck fails even though the chain itself is valid. Consider passing the appropriate preceding hops (or a socket override) so each hop’s host-key verification uses the same jump route as the actual connection.

Useful? React with 👍 / 👎.

Copy link

@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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/core/utils/server.dart`:
- Around line 188-197: The loop unconditionally writes entries for s.oldId into
injectedSpiMap (and injectedKeyMap) which can add an empty-string key; update
the loop that iterates jumpChain so you only set injectedSpiMap[s.oldId] = s and
injectedKeyMap[s.oldId] = jumpPrivateKeys[i] when s.oldId.isNotEmpty (mirror the
guard used in resolveMergedJumpChain) to avoid introducing an empty-string key
that breaks resolveSpi lookups.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2eeb55c and 09f1ab2.

📒 Files selected for processing (2)
  • lib/core/utils/server.dart
  • lib/data/model/sftp/worker.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/data/model/sftp/worker.dart
🔇 Additional comments (6)
lib/core/utils/server.dart (6)

41-69: LGTM: Jump chain resolution with proper error handling.

The resolveMergedJumpChain function correctly builds a map from the provided jumpChain and handles both id and oldId lookups. The nested resolveSpi function properly distinguishes between isolate mode (where jumpChain is required) and normal mode (where Stores can be accessed).


71-110: LGTM: DFS-based chain resolution with proper cycle detection.

The implementation correctly uses a two-set approach (seen for visited nodes, stack for current DFS path) to detect cycles while avoiding duplicate processing. The fallback from jumpChainIds to jumpId (line 75, 94) maintains backward compatibility with single-hop configurations.


112-150: LGTM: Clean public API with internal delegation.

The genClient public function properly initializes the visited set and delegates to _genClientInternal. The documentation clearly explains the purpose of jumpChain and jumpPrivateKeys parameters for isolate scenarios where Stores is unavailable.


220-286: Resource cleanup issue addressed.

The multi-hop chain building now properly tracks intermediate clients in createdClients (lines 245, 269) and closes them all on error (lines 275-280). The comment at lines 284-285 correctly explains that intermediate clients must remain open on success because the returned socket tunnels through them.

This addresses the previously flagged resource leak concern.


499-539: Host-key verification now properly traverses the full jump chain.

The ensureKnownHostKey function now:

  1. Uses resolveMergedJumpChain(spi) (line 506) to resolve both jumpChainIds and the legacy jumpId — addressing the previously raised concern about ignoring jumpChainIds.
  2. Routes each hop's host-key check through its preceding hops by passing precedingHops as jumpChain (lines 512-523) — addressing the concern about attempting direct TCP connections to intermediate hops that are only reachable through earlier hops.

This implementation correctly ensures all hop fingerprints are verified before establishing the full chain.


541-570: LGTM: Per-hop host-key verification with proper cleanup.

The _ensureKnownHostKeyForSingle helper correctly:

  • Short-circuits if the fingerprint is already cached (line 549-551)
  • Passes jumpChain/jumpPrivateKeys to route the connection through prior hops
  • Uses try-finally to ensure the client is closed after verification (lines 562-566)
  • Returns the updated cache for use by subsequent iterations

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@lollipopkit
Copy link
Owner Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52c80795f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 270 to 272
}

return await currentClient!.forwardLocal(spi.ip, spi.port);

Choose a reason for hiding this comment

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

P2 Badge Close jump-hop clients when target disconnects

In the multi-hop branch, hop clients are added to createdClients but on success the function immediately returns the forwarded socket without retaining any reference to those clients. As a result, when the caller later closes the target SSHClient, the intermediate hop sessions remain open with no way to close them, leaking SSH connections for every hop in the chain. This occurs whenever a user connects through a jump chain and then disconnects. Consider tying hop client lifetimes to the returned client (e.g., close them when the target client closes) or returning a wrapper that can dispose the hop chain.

Useful? React with 👍 / 👎.

@lollipopkit
Copy link
Owner Author

@coderabbitai Why still blocking?

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check current PR status including review decisions and CI
echo "=== PR Status and Mergeable State ==="
gh pr view 1006 --json state,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup --jq '{state, isDraft, mergeable, mergeStateStatus, reviewDecision, checks: .statusCheckRollup}'

echo -e "\n=== CI Checks Status ==="
gh pr checks 1006

echo -e "\n=== All Reviews Summary ==="
gh api repos/:owner/:repo/pulls/1006/reviews --jq 'group_by(.user.login) | map({user: .[0].user.login, latest_state: (sort_by(.submitted_at) | last.state), submitted_at: (sort_by(.submitted_at) | last.submitted_at)}) | .[]'

echo -e "\n=== Unresolved Review Comment Threads ==="
gh api graphql -f query='
query {
  repository(owner: "lollipopkit", name: "flutter_server_box") {
    pullRequest(number: 1006) {
      reviewThreads(first: 50) {
        nodes {
          isResolved
          comments(first: 10) {
            nodes {
              author {
                login
              }
              body
              path
              line
              createdAt
            }
          }
        }
      }
    }
  }
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {isResolved, author: .comments.nodes[0].author.login, path: .comments.nodes[0].path, line: .comments.nodes[0].line, preview: .comments.nodes[0].body[0:150]}'

Length of output: 2862


@lollipopkit Good news! The CI checks are now passing ✅ (flutter analysis succeeded).

However, the PR is still blocked because there are 6 unresolved review comment threads, keeping the review status at CHANGES_REQUESTED:

📝 Unresolved Review Threads

From Codex (chatgpt-codex-connector):

  1. 🟠 P1 - lib/core/utils/server.dart - ensureKnownHostKey needs to traverse jumpChainIds instead of just jumpId
  2. 🟡 P2 - lib/core/utils/server.dart (line 535) - Host-key checks should route through prior hops
  3. 🟡 P2 - lib/core/utils/server.dart - Jump-hop clients should be closed when target disconnects
  4. 🟠 P1 - lib/data/model/sftp/req.dart - SFTP jumpChain doesn't include nested hop chains when running in isolate
  5. 🟡 P2 - lib/view/page/server/edit/jump_chain.dart - Cycle check only checks direct duplicates, doesn't traverse nested jump chains

From CodeRabbit (me):

  1. 🟡 Minor - lib/data/model/sftp/req.dart - Inconsistent error handling (silent truncation vs throwing)

✅ To Unblock the PR:

You need to either:

  • Address the review comments by making code changes, or
  • Reply to the threads to discuss/clarify why they may not apply

The Codex comments appear to be the primary blockers (especially the P1 priority ones). Would you like me to take a closer look at any specific thread to help you understand what changes are needed?

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.

希望能支持多级跳板服务器

2 participants