Skip to content

feat: add discoverable terminal tabs#33

Open
salemsayed wants to merge 1 commit into
jossephus:mainfrom
salemsayed:implement/tab-strip-upstream
Open

feat: add discoverable terminal tabs#33
salemsayed wants to merge 1 commit into
jossephus:mainfrom
salemsayed:implement/tab-strip-upstream

Conversation

@salemsayed
Copy link
Copy Markdown
Contributor

@salemsayed salemsayed commented Jun 2, 2026

Summary

Adds an opt-in discoverable terminal tab strip while keeping the existing classic terminal tab behavior as the default.

What changed:

  • Adds a terminal tab mode setting so users can choose between the existing classic tab behavior and the new visible tab strip.
  • Adds a global top tab strip for connected, connecting, reconnecting, disconnected, error, and empty terminal states.
  • Adds an in-terminal server picker for opening new SSH sessions without leaving the terminal screen.
  • Adds a global tab manager for switching, closing, and reviewing terminal tabs.
  • Preserves existing connect verification behavior for picker-launched connections.
  • Shares command-palette hint styling with the new terminal picker/manager surfaces.
  • Hardens backup import/export fallback compatibility so backup flows keep working when native backup symbols are unavailable or stale.
  • Cleans up related lint/test debt found during validation.

Implementation notes:

  • No Room migration is needed.
  • Classic terminal tabs remain the factory default; the new visible strip is opt-in through Settings.
  • The Settings backup row remains compact and unchanged visually apart from the existing backup feature.
  • APK validation now confirms the Chuchu JNI library is packaged before sending tester builds.

Testing

Validation performed locally:

  • git diff --check
  • PATH=/home/salem/.cache/chuchu-tools/zig-x86_64-linux-0.15.2:$PATH ANDROID_NDK_HOME=/home/salem/Android/Sdk/ndk/27.1.12297006 ANDROID_NDK_ROOT=/home/salem/Android/Sdk/ndk/27.1.12297006 make build
  • cd android && ./gradlew app:compileDebugKotlin app:testDebugUnitTest app:assembleDebug app:lintDebug
  • Verified debug APK contains lib/arm64-v8a/libchuchu_jni.so.

User testing:

  • Built and sent debug APKs for on-device testing.
  • Follow-up tester findings were fixed before opening this PR:
    • the APK now packages libchuchu_jni.so;
    • the Manage Backups button is compact instead of stretched;
    • backup import/export safely falls back when native backup methods are unavailable.

AI disclosure

This PR was implemented by AI at the user's request. The user tested debug APKs on-device and requested that AI involvement be disclosed.

Summary by CodeRabbit

  • New Features

    • Terminal tab interface setting (Classic / Strip) with persistent setting and UI controls
    • Strip mode: horizontal tab strip, server picker, and full-screen tab manager (select/duplicate/close tabs)
    • Command palette shows keyboard hints
  • Bug Fixes / Reliability

    • Backup: native encryption/decryption now falls back to a JVM implementation for greater robustness
  • Tests

    • Added cryptography validation tests for backup encryption/decryption

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Warning

Review limit reached

@salemsayed, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 minutes and 44 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37e68e78-d7d8-42be-8cd5-ab0b6eb0cfc2

📥 Commits

Reviewing files that changed from the base of the PR and between 33ea210 and 83a3f31.

📒 Files selected for processing (18)
  • android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/ssh/RsaKeyGenerator.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionEngine.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/PaletteHint.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalServerPicker.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabMode.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabStrip.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalViewModel.kt
  • android/app/src/test/java/com/jossephus/chuchu/data/backup/ChuchuBackupCoreTest.kt
  • zig-src/src/bridge/chuchu_backup.zig
📝 Walkthrough

Walkthrough

This PR adds a configurable terminal tab display mode (Classic tabbed layout vs. Strip overlay mode) with supporting UI components, settings persistence, and auth-gated host selection. It also introduces pure-JVM AES-256-GCM backup encryption as a fallback when native support is unavailable, updates SSH key encoding to Android's Base64 library, and refactors terminal state handling for lifecycle awareness.

Changes

Terminal Tab Mode System and Session Updates

Layer / File(s) Summary
Tab mode enum and settings persistence
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabMode.kt, android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt
TerminalTabMode enum defines Classic and Strip modes. SettingsRepository adds terminalTabMode StateFlow persisted to SharedPreferences via KEY_TAB_MODE, with safe parsing fallback to default mode.
Strip-mode UI components
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/PaletteHint.kt, android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabStrip.kt, android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalServerPicker.kt, android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt
New composables for strip-mode overlays: TerminalTabStrip renders persistent horizontal tab bar with status dots; TerminalServerPicker loads and displays saved hosts for selection; TerminalTabManager provides global full-screen tab overlay with keyboard navigation; PaletteHint extracted as reusable key-label component. All use status color/label mappers.
TerminalScreen integration
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt
Loads terminalTabMode and requireAuthOnConnectSetting from settings; renders UI by mode (strip shows persistent tab strip with overlay pickers; classic shows centered layout); hoists server picker and tab manager outside main state when; gates host opening behind optional user verification based on per-host and user settings; refactors passphrase dialog to track picker origin and conditionally call onBack(); updates key handling for both overlay types and mode-specific navigation.
Settings UI for tab mode
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt, android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt, android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt
SettingsScreen and TerminalSettings (in TerminalAccessorySettings) accept currentTabMode and onTabModeChanged callback; new UI card renders mode selector buttons iterating TerminalTabMode.entries; ApplicationNavController wires tab mode state from SettingsRepository through the settings route.
Terminal state and supporting changes
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt, android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalViewModel.kt, android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionEngine.kt, android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionRepository.kt
CommandPalette switches from eager tab.sessionState.value to collectAsStateWithLifecycle() for lifecycle-aware state; TerminalViewModel adds duplicateTab(tabId) for strip-mode tab duplication; TerminalSessionEngine removes unused _userHomeDir: Path parameter; TerminalSessionRepository.openTab adapts constructor call.
Backup encryption native/JVM fallback
android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt, android/app/src/test/java/com/jossephus/chuchu/data/backup/ChuchuBackupCoreTest.kt, zig-src/src/bridge/chuchu_backup.zig
ChuchuBackupCodec.encrypt/decrypt now prefer native bridge but gracefully fall back to pure-JVM AES-256-GCM if native unavailable (UnsatisfiedLinkError or not loaded). JVM implementation includes container metadata encoding, HMAC-SHA1 KDF, AEAD with AAD, and dual-passphrase support (UTF-16BE primary, UTF-8 legacy fallback). Tests validate container metadata layout, key derivation against known vectors, and tampering detection via metadata byte mutation. Zig bridge exported symbols renamed to reflect updated Java package path com_jossephus_chuchu_service_backup_*.
SSH key encoding platform switch
android/app/src/main/java/com/jossephus/chuchu/service/ssh/RsaKeyGenerator.kt
Replaces java.util.Base64 with android.util.Base64 for both private key PEM (encoded NO_WRAP then re-chunked to 64-char lines) and OpenSSH public key payload.

Sequence Diagrams

sequenceDiagram
  participant User
  participant TerminalScreen
  participant TerminalServerPicker
  participant AuthService
  participant SessionRepo
  
  User->>TerminalScreen: Opens terminal in strip mode
  TerminalScreen->>TerminalScreen: Load terminalTabMode from settings
  TerminalScreen->>TerminalScreen: Render TerminalTabStrip at top
  
  User->>TerminalScreen: Press 't' key to add connection
  TerminalScreen->>TerminalServerPicker: Show picker overlay
  TerminalServerPicker->>TerminalServerPicker: Load hosts from database
  User->>TerminalServerPicker: Select host
  TerminalServerPicker->>TerminalScreen: onHostSelected(TabSpec)
  
  TerminalScreen->>TerminalScreen: Enrich TabSpec with SSH keys
  alt requireAuthOnConnect enabled
    TerminalScreen->>AuthService: requireUserVerification
    AuthService->>TerminalScreen: VerificationResult
  end
  
  TerminalScreen->>SessionRepo: openTab(TabSpec)
  SessionRepo->>TerminalScreen: Return TabSession
  TerminalScreen->>TerminalScreen: Add tab to active list
Loading
flowchart TD
  A["encrypt/decrypt called"] --> B{"Native bridge<br/>loaded?"}
  B -->|Yes| C["Attempt native<br/>encryption"]
  C --> D{"UnsatisfiedLinkError<br/>or unavailable?"}
  D -->|No| E["Return native result"]
  D -->|Yes| F["Fall through"]
  B -->|No| F
  F --> G["encryptWithJvm or<br/>decryptWithJvm"]
  G --> H{"Container format<br/>valid?"}
  H -->|Yes, decrypt| I["Derive AES key<br/>HMAC-SHA1-KDF"]
  I --> J["AES-256-GCM<br/>decrypt with AAD"]
  J --> K{"AEAD tag<br/>valid?"}
  K -->|Yes| L["Return plaintext"]
  K -->|No| M["InvalidBackupPassphraseException"]
  H -->|No| N["BackupFormatException"]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • jossephus/chuchu#23: Modifies ChuchuBackupCodec—the current PR adds JVM fallback for encrypt/decrypt and related tests.
  • jossephus/chuchu#9: Also modifies terminal tab UI and CommandPalette state handling; related to UI changes here.

Poem

🐰 A rabbit hops through tab strips so neat,
Classic and strip modes dance in the heat,
Backups fall back when native's away,
Android Base64 saves the day!
SSH keys hop, settings persist—
Terminal tabs won't be missed! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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: add discoverable terminal tabs' clearly and concisely summarizes the main feature addition across the changeset—a new discoverable/visible terminal tab interface.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@gitguardian
Copy link
Copy Markdown

gitguardian Bot commented Jun 2, 2026

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@salemsayed salemsayed mentioned this pull request Jun 2, 2026
Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt (1)

1512-1542: ⚡ Quick win

Simplify dispatcher switching: remove redundant coroutine launches.

The code switches to Dispatchers.Main (line 1528) and then immediately launches a new coroutine on Dispatchers.Main (line 1535). Since we're already on the Main dispatcher after withContext(Dispatchers.Main), the pickerScope.launch(Dispatchers.Main) wrapper is redundant. Similarly, line 1540 can call openOrPrompt directly.

♻️ Simplify by calling openOrPrompt directly
                     val mustVerify = requireAuthOnConnectSetting || hostRequiresAuth
                     if (mustVerify) {
                         withContext(Dispatchers.Main) {
                             requireUserVerification(
                                 context = context,
                                 title = "Verify to connect",
                                 subtitle = "Authenticate to open this server session",
                             ) { result ->
                                 if (result == VerificationResult.Success) {
-                                    pickerScope.launch(Dispatchers.Main) { openOrPrompt(enrichedSpec) }
+                                    openOrPrompt(enrichedSpec)
                                 }
                             }
                         }
                     } else {
-                        withContext(Dispatchers.Main) { openOrPrompt(enrichedSpec) }
+                        withContext(Dispatchers.Main) {
+                            openOrPrompt(enrichedSpec)
+                        }
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt`
around lines 1512 - 1542, Inside the Dispatchers.IO block you switch to
Dispatchers.Main with withContext, then redundantly launch a new Main coroutine;
remove the nested pickerScope.launch(Dispatchers.Main) and call
openOrPrompt(enrichedSpec) directly in the requireUserVerification Success
callback, and likewise call openOrPrompt(enrichedSpec) directly inside the
existing withContext(Dispatchers.Main) else branch; keep the outer
pickerScope.launch(Dispatchers.IO), requireUserVerification(...) and
withContext(Dispatchers.Main) surrounding code unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt`:
- Around line 242-247: In the InvalidBackupPassphraseException fallback, avoid
creating an immutable String via passphrase.concatToString(); instead encode the
CharArray directly to UTF-8 bytes and avoid intermediate String: wrap the
passphrase with CharBuffer.wrap(passphrase), use
StandardCharsets.UTF_8.encode(charBuf) to get a ByteBuffer, copy bb.remaining()
bytes into a new ByteArray (legacyPassphraseBytes) via
bb.get(legacyPassphraseBytes), then call decryptContainer(container,
legacyPassphraseBytes) and finally securely wipe legacyPassphraseBytes.fill(0);
also clear/overwrite the ByteBuffer backing array if present (bb.clear() and if
bb.hasArray() fill bb.array() with zeros) to ensure no leftover plaintext
remains.

---

Nitpick comments:
In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt`:
- Around line 1512-1542: Inside the Dispatchers.IO block you switch to
Dispatchers.Main with withContext, then redundantly launch a new Main coroutine;
remove the nested pickerScope.launch(Dispatchers.Main) and call
openOrPrompt(enrichedSpec) directly in the requireUserVerification Success
callback, and likewise call openOrPrompt(enrichedSpec) directly inside the
existing withContext(Dispatchers.Main) else branch; keep the outer
pickerScope.launch(Dispatchers.IO), requireUserVerification(...) and
withContext(Dispatchers.Main) surrounding code unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1e1663a-9799-485a-837a-4ced82083fef

📥 Commits

Reviewing files that changed from the base of the PR and between 6314b09 and 5d92a4a.

📒 Files selected for processing (18)
  • android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/ssh/RsaKeyGenerator.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionEngine.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/PaletteHint.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalServerPicker.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabMode.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabStrip.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalViewModel.kt
  • android/app/src/test/java/com/jossephus/chuchu/data/backup/ChuchuBackupCoreTest.kt
  • zig-src/src/bridge/chuchu_backup.zig
💤 Files with no reviewable changes (2)
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionEngine.kt

@salemsayed salemsayed force-pushed the implement/tab-strip-upstream branch from 5d92a4a to 33ea210 Compare June 2, 2026 06:13
Copy link
Copy Markdown

@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

🧹 Nitpick comments (6)
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt (3)

190-195: ⚡ Quick win

Add stable keys to LazyColumn items for efficient recomposition.

Using items(entries.size) without keys can cause unnecessary recompositions and animation issues when tabs are added/removed/reordered. The TabSession.id provides a natural stable key.

Suggested fix
                     LazyColumn(
                         state = listState,
                         modifier = Modifier.fillMaxWidth().heightIn(max = 360.dp),
                     ) {
-                        items(entries.size) { idx ->
-                            val tab = entries[idx]
+                        items(entries, key = { it.id }) { tab ->
+                            val idx = entries.indexOf(tab)
                             val isActive = tab.id == activeTabId

Alternatively, if index is needed for focus comparison, use itemsIndexed:

-                        items(entries.size) { idx ->
-                            val tab = entries[idx]
+                        itemsIndexed(entries, key = { _, tab -> tab.id }) { idx, tab ->
                             val isActive = tab.id == activeTabId
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt`
around lines 190 - 195, The LazyColumn currently uses items(entries.size) which
lacks stable keys; update the call to use items(items = entries, key = { it.id
}) (or itemsIndexed(entries, key = { _, item -> item.id } if you need the index
for focus logic) so each TabSession uses its TabSession.id as the stable key;
change the block around LazyColumn/entries/items to pass entries directly and
supply the key lambda to avoid unnecessary recompositions and animation
glitches.

141-149: 💤 Low value

PageUp/PageDown navigate by single item instead of page-sized jumps.

Key.PageUp and Key.PageDown currently move focus by ±1 index, identical to arrow keys. For longer tab lists, users may expect page-sized jumps (e.g., ±5 items or viewport-based).

This is minor since the list is capped at 360dp height, limiting visible items.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt`
around lines 141 - 149, Currently Key.PageUp/Key.PageDown are handled exactly
like arrow keys; change their branch in TerminalTabManager.kt so that when
event.type == KeyEventType.KeyUp and entries.isNotEmpty() you adjust
focusedTabIndex by a page jump rather than ±1: compute a pageSize (suggest using
a constant like 5 or derive from visible item count) then call
onFocusedTabIndexChange((focusedTabIndex - pageSize).mod(entries.size)) for
PageUp and onFocusedTabIndexChange((focusedTabIndex +
pageSize).mod(entries.size)) for PageDown; preserve the Key.DirectionUp/Down
behavior and the modulo wrapping using entries.size.

405-414: 💤 Low value

Keep statusDotColor consistent with TerminalTabStrip

TerminalTabManager.statusDotColor maps SessionStatus to colors exactly the same way as TerminalTabStrip (Connected→success, Connecting/Reconnecting→accent, Disconnected/Error→error). Consider extracting the shared mapping to a common helper to prevent future drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt`
around lines 405 - 414, statusDotColor in TerminalTabManager duplicates the
SessionStatus→Color mapping used in TerminalTabStrip; extract the shared mapping
into a single helper (e.g., mapSessionStatusToColor or
SessionStatus.colorForPalette) and have both TerminalTabManager.statusDotColor
and TerminalTabStrip call that helper so the
Connected/Connecting/Reconnecting/Disconnected/Error cases are defined in one
place to prevent drift.
android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt (1)

874-901: 💤 Low value

Move/guard the strip “no terminal sessions” empty-state out of Connected/Reconnecting

  • TerminalSessionRepository.sessionState is derived from activeTab; when _tabs is empty, activeTab becomes null and sessionState switches to SessionState() (not the active tab’s session state).
  • That makes tabMode == TerminalTabMode.Strip && tabs.isEmpty() inside SessionStatus.Connected, SessionStatus.Reconnecting at most a transient timing/recomposition artifact; move this UI to the disconnected/no-active-tab path (or guard it with sessionState.status) to avoid dead/contradictory state rendering.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt`
around lines 874 - 901, The empty-state UI block that checks "tabMode ==
TerminalTabMode.Strip && tabs.isEmpty()" is being rendered inside the
Connected/Reconnecting branches and can flash due to
TerminalSessionRepository.sessionState being derived from activeTab (which
becomes null when _tabs is empty); move this empty-state UI out of the
Connected/Reconnecting rendering path into the disconnected/no-active-tab branch
(where sessionState.status indicates not connected) or add an explicit guard
using sessionState.status (e.g., only show when sessionState.status is
Disconnected/NoActiveTab) so the "+ new connection" strip state is only rendered
when there truly is no active session, referencing TerminalTabMode.Strip,
tabs.isEmpty(), sessionState and activeTab to locate and fix the logic.
android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt (2)

339-365: 💤 Low value

Consider wiping intermediate key derivation arrays.

The u and block arrays hold key derivation intermediates that could theoretically aid key recovery if memory is compromised. While less sensitive than the passphrase itself, wiping them aligns with the defensive approach already applied to keyBytes and passphraseBytes.

🔒 Suggested improvement
             val copySize = minOf(block.size, output.size - generated)
             block.copyInto(output, destinationOffset = generated, startIndex = 0, endIndex = copySize)
             generated += copySize
             blockIndex += 1
+            block.fill(0)
+            u.fill(0)
         }
         output
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt`
around lines 339 - 365, The deriveAesKey function leaves intermediate byte
arrays (u and block) in memory; after each block is XORed and copied into
output, overwrite those arrays (e.g., call fill(0) on u and block) to zeroize
sensitive intermediates before they go out of scope, and also ensure any
leftover buffers are cleared in the catch/finally path so u/block cannot remain
in memory after deriveAesKey returns.

532-536: 💤 Low value

readSizedBytes is currently unused

readSizedBytes in ChuchuBackupCodec.kt is only present as its own definition (no other Kotlin references/call sites found). If it’s not intended for immediate use, remove it; otherwise document the intended future use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt`
around lines 532 - 536, The function readSizedBytes is defined but unused;
either remove it or document its intended future use and keep it. If removing,
delete the readSizedBytes(...) definition from ChuchuBackupCodec.kt and run a
project-wide search to ensure no hidden references; if keeping, add a KDoc above
readSizedBytes explaining its purpose and expected call sites (e.g., for
fixed-length fields), and mark it with `@Suppress`("unused") if you want to avoid
linter warnings until it’s referenced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt`:
- Around line 1512-1542: The biometric callback from requireUserVerification can
execute after the picker/composable has been dismissed, so guard against stale
callbacks by tracking picker lifecycle: introduce a boolean/AtomicBoolean (e.g.,
pickerActive or reuse showServerPicker) set true before launching the
pickerScope coroutine and set false when the picker is closed/unmounted, then in
the requireUserVerification callback check result == VerificationResult.Success
&& pickerActive (or lifecycleOwner.isAtLeast(STARTED)) before calling
openOrPrompt(enrichedSpec); update code around pickerScope,
requireUserVerification, and openOrPrompt to consult that flag rather than
unconditionally invoking openOrPrompt.

---

Nitpick comments:
In
`@android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt`:
- Around line 339-365: The deriveAesKey function leaves intermediate byte arrays
(u and block) in memory; after each block is XORed and copied into output,
overwrite those arrays (e.g., call fill(0) on u and block) to zeroize sensitive
intermediates before they go out of scope, and also ensure any leftover buffers
are cleared in the catch/finally path so u/block cannot remain in memory after
deriveAesKey returns.
- Around line 532-536: The function readSizedBytes is defined but unused; either
remove it or document its intended future use and keep it. If removing, delete
the readSizedBytes(...) definition from ChuchuBackupCodec.kt and run a
project-wide search to ensure no hidden references; if keeping, add a KDoc above
readSizedBytes explaining its purpose and expected call sites (e.g., for
fixed-length fields), and mark it with `@Suppress`("unused") if you want to avoid
linter warnings until it’s referenced.

In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt`:
- Around line 874-901: The empty-state UI block that checks "tabMode ==
TerminalTabMode.Strip && tabs.isEmpty()" is being rendered inside the
Connected/Reconnecting branches and can flash due to
TerminalSessionRepository.sessionState being derived from activeTab (which
becomes null when _tabs is empty); move this empty-state UI out of the
Connected/Reconnecting rendering path into the disconnected/no-active-tab branch
(where sessionState.status indicates not connected) or add an explicit guard
using sessionState.status (e.g., only show when sessionState.status is
Disconnected/NoActiveTab) so the "+ new connection" strip state is only rendered
when there truly is no active session, referencing TerminalTabMode.Strip,
tabs.isEmpty(), sessionState and activeTab to locate and fix the logic.

In
`@android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt`:
- Around line 190-195: The LazyColumn currently uses items(entries.size) which
lacks stable keys; update the call to use items(items = entries, key = { it.id
}) (or itemsIndexed(entries, key = { _, item -> item.id } if you need the index
for focus logic) so each TabSession uses its TabSession.id as the stable key;
change the block around LazyColumn/entries/items to pass entries directly and
supply the key lambda to avoid unnecessary recompositions and animation
glitches.
- Around line 141-149: Currently Key.PageUp/Key.PageDown are handled exactly
like arrow keys; change their branch in TerminalTabManager.kt so that when
event.type == KeyEventType.KeyUp and entries.isNotEmpty() you adjust
focusedTabIndex by a page jump rather than ±1: compute a pageSize (suggest using
a constant like 5 or derive from visible item count) then call
onFocusedTabIndexChange((focusedTabIndex - pageSize).mod(entries.size)) for
PageUp and onFocusedTabIndexChange((focusedTabIndex +
pageSize).mod(entries.size)) for PageDown; preserve the Key.DirectionUp/Down
behavior and the modulo wrapping using entries.size.
- Around line 405-414: statusDotColor in TerminalTabManager duplicates the
SessionStatus→Color mapping used in TerminalTabStrip; extract the shared mapping
into a single helper (e.g., mapSessionStatusToColor or
SessionStatus.colorForPalette) and have both TerminalTabManager.statusDotColor
and TerminalTabStrip call that helper so the
Connected/Connecting/Reconnecting/Disconnected/Error cases are defined in one
place to prevent drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc7cab2c-7985-4c66-a2fd-75eb2a3c9614

📥 Commits

Reviewing files that changed from the base of the PR and between 5d92a4a and 33ea210.

📒 Files selected for processing (18)
  • android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/backup/ChuchuBackupCodec.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/ssh/RsaKeyGenerator.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionEngine.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/ApplicationNavController.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/PaletteHint.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalScreen.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalServerPicker.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabManager.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabMode.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabStrip.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalViewModel.kt
  • android/app/src/test/java/com/jossephus/chuchu/data/backup/ChuchuBackupCoreTest.kt
  • zig-src/src/bridge/chuchu_backup.zig
💤 Files with no reviewable changes (1)
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionRepository.kt
🚧 Files skipped from review as they are similar to previous changes (12)
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalViewModel.kt
  • zig-src/src/bridge/chuchu_backup.zig
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/TerminalAccessorySettings.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabMode.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/ssh/RsaKeyGenerator.kt
  • android/app/src/main/java/com/jossephus/chuchu/service/terminal/TerminalSessionEngine.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalTabStrip.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/CommandPalette.kt
  • android/app/src/main/java/com/jossephus/chuchu/data/repository/SettingsRepository.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Settings/SettingsScreen.kt
  • android/app/src/main/java/com/jossephus/chuchu/ui/screens/Terminal/TerminalServerPicker.kt
  • android/app/src/test/java/com/jossephus/chuchu/data/backup/ChuchuBackupCoreTest.kt

@salemsayed salemsayed force-pushed the implement/tab-strip-upstream branch from 33ea210 to 83a3f31 Compare June 2, 2026 06:27
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