Auth flow rewritten to be fully server-driven: no pre-connect password
dialog. TCP + KEX + host key verification happen first; password prompt
only appears when the server actually asks via keyboard-interactive or
the new prompted-password fallback (for servers that only accept the
"password" SSH method). Up to 3 password attempts per connection,
matching OpenSSH's NumberOfPasswordPrompts default.
"Remember password" checkbox now functional: AuthPromptResult threads
the remember flag through the callback chain; SSHSession stashes the
typed password in pendingRememberPassword; TerminalService persists it
to CredentialStore only after session.connect() succeeds — wrong
passwords are never saved.
Removed dead pre-connect dialog code: PasswordDialog composable,
PasswordResult, TerminalDialogRequest.PasswordPrompt, and
passwordPromptHandler.
Added docs/LEGACY_TEST_LAB.md: self-contained 2100-line guide for
building a dedicated server with 56 historical/modern Unix systems
for terminal parser conformance testing (Docker, QEMU/KVM, SIMH,
polarhome, IBM PowerVS). Includes all Dockerfiles, compose.yml,
SIMH configs, systemd units, and helper scripts inline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ViewPager2 rotation phantom (Google Issue 175796502):
- KeyboardView.onSizeChanged rebuilds buildPages() on any width
transition including initial 0→actual layout, not just on resize.
The first buildPages() from setup() runs at width=0 (view still
detached), VP2's internal RecyclerView can't reliably re-anchor to
the real dimensions that arrive later, leaving a fractional scroll
offset that leaks the neighbor page on the right edge. A second
buildPages() once the real size is known produces a clean result.
- KeyboardView.lastModifierStates caches CTRL/ALT/SHIFT state so it
survives the rebuild.
- Works around an unfixed VP2 1.1.0 bug for non-FragmentStateAdapter
usage. Fresh launches and all rotation paths confirmed clean on S23.
NumBlok toggle on mini numpad:
- KeyDefinition gains numLabel/numAction optional fields
- KeyAction gains ToggleNumBlock data object
- LayoutParser understands "numLabel"/"numAction" JSON and the
"toggle_numblock" action type
- KeyboardPageView.numBlockActive swaps label rendering; toggle key
gets LOCKED-style amber glow while active
- TerminalKeyboard.numBlockActive state; handleKeyAction swaps
action→numAction; attachMiniTo carries state across rotation rebuilds
- Mini last row: Num 0 \ (base) → Num Ins ~ (numblock)
- Rows 1-3 map to PC-keypad nav: Home/↑/PgUp, ←/Esc/→, End/↓/PgDn
- QuickBarCustomizer serializer/deserializer learned toggle_numblock
Hardware keyboard auto-hide:
- MainActivity derives hasHwKeyboard from LocalConfiguration.current.
keyboard != KEYBOARD_NOKEYS && hardKeyboardHidden != HARDKEYBOARDHIDDEN_YES
- LaunchedEffect(hasHwKeyboard) { ckbHidden = hasHwKeyboard } seeds
the toggle on every HW kb connect/disconnect
- In HW kb mode the kebab Show/Hide controls both CKB and QuickBar as
a pair: val qbShownByUser = if (hasHwKeyboard) !ckbHidden else
quickBarVisible
- Normal mode unchanged (QB follows its own quickBarVisible pref)
Number row dropdown:
- KeyboardSettingsDialog now shows all four options (top/left/right/
hidden) in both orientations. Previously left/right were filtered
out in portrait, preventing the user from setting the mini mode
without first rotating into landscape.
- Portrait-override on the effective numberRowMode stays in place
(mini is landscape-only by design — documented in CLAUDE.md and
auto-memory).
Audit fixes:
- KeyboardPageView: removed key.hint!! by capturing into a local
hintText val so smart-cast flows through the render branches
- SSHKeyLoader.loadEd25519FromPkcs8: require(seed.size == 32) so a
malformed PKCS#8 blob fails fast with a clear message instead of
crashing deep inside EdDSAPrivateKeySpec
- vault_crypto.cpp nativeEncrypt: secure_zero(ptPtr, ptLen) before
ReleaseByteArrayElements on all three paths (success + two error
paths), matching the existing key-handling pattern for plaintext
defence-in-depth through the JNI boundary
Docs:
- TECHNICAL.md: NumBlok, VP2 rebuild workaround, HW keyboard auto-hide,
portrait mini override notes
- CLAUDE.md: lib-terminal-keyboard module description updated with
the same invariants
- TODO.md: recently-completed items through 2026-04-06
- Audit.md: investigation log including false positives for future
reference
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SFTP standalone sessions now use shared buildJumpChain for ProxyJump support,
start keepalive monitor for zombie detection, and have full disconnect/reconnect
UI with SSH state monitoring. Tab overflow menu adds "Connect to Terminal".
Fling scroll removes hard distance cap (flingMaxRows) so momentum decays
naturally. Selection toolbar Google button wired. Mouse reporting gated by
pro feature. Dead code (onSaveSnippet) removed. Write logging for paste debug.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical fix: SFTPClient.close() kills the parent SSH transport by
sending a channel EOF. Removed the close() call — just null out the
reference, SFTP subsystem cleans up when SSH connection closes.
Other SFTP fixes:
- Single SFTP session open (was opening twice — race corrupted transport)
- SftpSessionManager.open() is now suspend (no 500ms guesswork delay)
- SftpScreen always composed (preserves SAF launchers across picker)
- Keyboard/QuickBar hidden on SFTP tabs (tab type check)
- Disconnect notifications suppressed when app in foreground
- Title shows connection name (titleSmall style)
New SFTP features:
- File type icons (32dp): APK, image, video, audio, archive, PDF, code,
shell, keys, text — color-coded per type
- Multi-select: tap icon to select, batch download/delete bar
- Hidden files hidden by default, toggle in overflow menu
- i18n strings for hidden files toggle (EN/ES/SV)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CKB: temporary hide via ckbHidden state (not pref change), tap terminal to reshow
- CKB/QB input now scrolls terminal to bottom (was IME-only)
- Keyboard hints: all keys get hints (removed label length filter), smart
positioning (above by default, flips below if off-screen, clamped to edges)
- Long-press popup: auto-shrinks cells to fit screen width
- QB: hints on all keys (onKeyDown on ACTION_DOWN for all, not just repeatable),
scroll cancels key repeat
- PICK_HOST Back returns to terminal when sessions are open
- Copy Log ZIP now includes logcat.txt (last 5000 lines)
- TerminalKeyboard.attachTo() safe for re-attachment
- SFTP: long-press context menu, settings detail expansion, i18n additions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add --es profile "Name" launch intent to auto-connect by connection name/nickname
- Add --ez clearLog true intent extra to clear debug log on launch
- Add --ez dump true ADB broadcast to dump full screen buffer to log file
- Fix PTY allocation to use actual terminal dimensions instead of hardcoded 80x24:
SSHSession.resize() now stores dimensions even while Connecting, and PTY
allocation picks up the latest values instead of the initial config
- Add connectByProfile() to MainViewModel for profile-based connections
- Add VttestParityTest: replays recorded vttest session through both our engine
and Termux at 80x24 and 102x38, verifying cell-by-cell parity (both pass)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SSHSession.resize() checked currentPtyCols/Rows before scope.launch
but updated them inside the launched coroutine, so rapid back-to-back
calls from onSurfaceReady and onDimensionsChanged both passed the check
and sent duplicate changeWindowDimensions to the SSH server. Move the
update before the launch so the second call sees the already-set values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lib-ssh — SftpSession:
- Wraps SSHJ SFTPClient with coroutines API on single-threaded dispatcher
- listDirectory (folders first, sort by name/size/date), downloadFile,
uploadFile (64KB streaming with progress), createDirectory, rename,
delete, canonicalize
- SftpEntry data class with Unix permissions string formatting
- Reuses existing SSHClient via newSFTPClient() — no second TCP connection
app — SftpViewModel:
- Directory browsing with currentPath/entries/isLoading/error StateFlows
- Transfer tracking: TransferState with ACTIVE/COMPLETE/FAILED/CANCELLED
- Large file confirmation (≥5MB) via confirmLargeDownload SharedFlow
- Upload, download, mkdir, rename, delete, sort order, download URI prefs
app — SftpActivity (replaces stub):
- Dark terminal aesthetic, programmatic layout
- Path breadcrumb bar with tappable segments
- File list: folder/file icons, name, permissions, size, date
- Tap folder to navigate, tap file to download, long-press context menu
- Bottom action bar: Upload (SAF multi-file) + Download folder (SAF tree)
- Transfer progress cards with direction badges, cancel, retry, auto-dismiss
- SAF integration: persistable URI permissions, stale URI recovery
app — TerminalService SFTP session management:
- openSftpSession/getSftpSession/closeSftpSession methods
- Auto-cleanup on parent SSH session disconnect
22 new unit tests (SftpSession, SftpViewModel, SftpEntrySort)
25 new SFTP string resources in EN/ES/SV
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DefaultConfig is slow because it registers 31 ciphers and trial-inits
each via JCE. FastSSHConfig registers only modern algorithms (curve25519,
chacha20-poly1305, aes-ctr, sha2 MACs) — covers >99% of servers.
BouncyCastle moved to last provider so Android Conscrypt handles crypto.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SelectionMagnifier: zoomed chars around handle, green cursor highlight
always visible (even on spaces), smooth velocity, no flickering
- Paste-only toolbar: clipboard text preview below Paste button
- Google search replaces snippet button in selection toolbar
- Toolbar follows visible teardrop handle dynamically
- Scrollbar only grabbable when visible from active scrolling
- Long-press host always shows full context menu (Connect, Disconnect
All, Edit, Duplicate, Delete) regardless of active sessions
- Delete disconnects all sessions first
- Fix port field: remove select-all-on-recompose bug
- Add DefaultConfig timing log in SSHSession
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert treating > as a general CSI flag (broke line wrap at right edge).
Re-apply as targeted fix: CSI > sequences (kitty keyboard, modifyOtherKeys)
are consumed fully and silently ignored without affecting CSI dispatch.
Add session recovery via DataStore, SSH/Telnet reconnect logic,
disconnected-mode UI, DSR diagnostic logging, thread-safety annotations.
Add 8 regression tests (line wrap, CSI > handling) — 1883/1891 parity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix render race condition: pass onScreenUpdated callback into connectSSH/
startLocalShell so it's set before the output collection job starts
- Remove LAYER_TYPE_SOFTWARE on SurfaceView that created an opaque layer
hiding the Surface canvas
- Add proportional scrollbar on right edge (visible when scrollback > 0)
- Add jump-to-top/bottom overlay buttons that fade after 3s of inactivity
- Boost fling velocity 2.5x, reduce friction, increase max fling distance
- Add "Tap to reconnect" on clean session exit (was null click listener)
- Add SSH key management, connection manager UI, credential store,
keyboard improvements, and database schema v2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Switch SSHJ from HEARTBEAT to KEEP_ALIVE provider (tracks responses,
disconnects after 3 missed keepalives / 45s)
- Add 10s write timeout via executor — detects dead TCP sockets
- Add connection health monitor: logs keepalive status every 15s,
warns on 60s read silence after write
- Auto-reconnect on unexpected disconnect with backoff (5s, 10s, 30s)
- Show countdown in status overlay, tap to reconnect immediately
- Cache password in memory for auto-reconnect, clear on user disconnect
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>