New lib-vault-crypto module with Argon2id + AES-256-GCM in C via JNI.
Export screen (full Scaffold) with item selection, password mode (min 12
chars, upper/lower/digit/special), and QR mode (background generation,
save/share gating, confirmation dialog). Import via bottom sheet with
SAF file picker, password/QR decryption, and conflict resolution.
Kebab menu replaces settings icon in ConnectionListScreen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SFTP is now rendered as an independent tab (amber accent) in the terminal
layer instead of a NavHost route. Tabs support duplicate, rename, close.
Fix dual-keyboard bug where system keyboard appeared on top of custom
keyboard: set softInputEnabled=false on TerminalSurfaceView in CKB mode
so tapping the terminal no longer triggers the IME.
Add diagnostic logging for keyboard show/hide transitions, softInputEnabled
changes, and DataStore keyboard type persistence to debug field reports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tab ordering: separate _tabOrder List<Long> in ViewModel instead of
relying on Map iteration order (StateFlow conflates equal maps,
silently dropping reorders). Duplicates insert right after source tab.
Numbering: scan existing labels to find max (N), no counter state.
Closes (2), duplicates → (4). All gone → resets to (2).
Keyboard: factory starts kbContainer GONE, default keyboardType=""
to prevent flash of custom keyboard on system-keyboard users.
Menu: Duplicate and SFTP only shown when session is Connected.
Session picker: LazyColumn for scrollable list with many sessions.
Code cleanup: removed dead _sessionFontSizes StateFlow, removed
reorderSessionAfter from TerminalService, deduplicated IME handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When sessions.size changes, animateScrollTo(maxValue) scrolls the
horizontal tab bar to the end so the new duplicate tab is visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
surfaceChanged fires before layout settles, giving tiny dimensions
(e.g. 80x8 instead of 80x26). reflowIfNeeded would truncate the
buffer to these wrong dimensions. onDimensionsChanged fires ~200ms
later with the correct post-layout size and handles the reflow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The post { requestLayout() } ran on every recomposition, causing
continuous layout cycles that interfered with ScaleGestureDetector.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
KeyboardView (ViewPager2) may not measure correctly if its container
was INVISIBLE during initial layout. Post requestLayout() on the
keyboard container whenever the session becomes visible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was setting fontSizeSp on the new SessionEntry AFTER connectSSH
returned — race with TerminalPane factory which reads it at creation.
Now passed as parameter so it's set at SessionEntry construction time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
onZoomResize wrote to SessionEntry but never called setSessionFontSize
which writes to Room. Re-added onZoomChanged callback from TerminalPane
to MainActivity so pinch zoom saves to both SessionEntry AND Room
(when single session for host).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ADB receiver now accepts --es fontsize "8.0" to set per-session zoom.
Cursor log now includes font size and grid dimensions for debugging.
Verified font persistence across:
- Back-exit + reopen: 8sp persisted via Room (72x25 grid) ✅
- Force-stop + reopen: 8sp persisted via Room ✅
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Five gaps where fontSizeSp was lost:
1. connectSSHInternal destroys old SessionEntry (disconnectSession)
2. scheduleAutoReconnect removes old entry (sessions.remove)
3. MainViewModel.connect() never passed Room's saved value
4. MainViewModel.reconnectSession() didn't preserve from old entry
5. Race: observer seeds from Room in IO, factory reads on Main first
Fix: added fontSizeSp parameter to connectSSH/connectSSHInternal.
Set on SessionEntry at creation time — no race, no loss.
Every caller now passes the right value:
- connect(): reads from Room (saved?.fontSizeSp)
- reconnectSession(): reads from old SessionEntry
- duplicateSession(): reads from source SessionEntry
- scheduleAutoReconnect(): reads from old entry before remove
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added writeConnectBanner() that writes bright cyan text to the terminal
buffer so the user sees connection progress:
- Direct: "── Connecting to user@host:port ──"
- Jump hosts: "── Connecting to jump 1/2: user@jumphost:port ──"
- After jump chain: "── Connecting to user@destination:port ──"
Uses the same ANSI style as the reconnecting banner (bold bright cyan).
Visible underneath the spinner overlay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three-tier zoom persistence:
- View: live fontSizeSp on TerminalSurfaceView (view lifetime)
- Service: SessionEntry.fontSizeSp (survives Activity death)
- Room: SavedConnection.fontSizeSp (survives app restart)
Rules:
- Zoom always saves to SessionEntry
- If only 1 session for the host: also saves to Room (permanent)
- If 2+ sessions for same host: SessionEntry only (per-session)
- New connection: reads default from Room, seeds SessionEntry
- Duplicate: copies fontSizeSp from source SessionEntry
- View recreation: factory reads from SessionEntry before surface fires
Added Room migration 10→11 (fontSizeSp REAL column).
Documented three-tier model in TECHNICAL.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All three per-session overrides were stored only in ViewModel StateFlows
which die on Activity destruction (back-exit). Moved to
TerminalService.SessionEntry which lives in the foreground service:
- fontSizeSp: restored in TerminalSurfaceView factory before surface
callbacks fire, preventing wrong-dimension reflow
- customLabel: restored in session observer label resolution
- customThemeName: restored in session observer theme resolution
Saved on every user action (zoom pinch, rename dialog, theme picker).
Removed unused fontSizeOverride/onZoomChanged params from TerminalPane.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two causes of view recreation:
1. No key(sid) in the for loop — Compose used positional identity,
so adding/removing sessions reshuffled all views.
2. View.GONE destroys SurfaceView surfaces — switched to INVISIBLE
so surfaces (and their font size, scroll state) persist when
switching sessions or navigating to the connection list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TerminalPanes were removed from the tree when switching to NAV_HOST,
destroying all AndroidViews (surfaces, keyboards). Every return to
terminal recreated everything from scratch, losing zoom, scroll
position, and causing unnecessary work.
Now the terminal layer stays in the tree with alpha(0) when hidden.
Views persist across navigation — zoom, scroll, keyboard state all
survive naturally without save/restore plumbing. Removed the
fontSizeOverride/onZoomChanged params since views are never recreated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Removed aggressive auto-switch in checkSessionRecovery that forced
terminal pane whenever sessions existed, ignoring back navigation.
Now only recovers from saved DataStore state (process death case).
2. Moved per-session font size storage from ViewModel (dies on back-exit)
to TerminalService.SessionEntry.fontSizeSp (survives Activity
destruction). Font sizes are restored from service entries when the
ViewModel loads sessions on rebind.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DropdownMenu popup was focusable by default, stealing focus from
the terminal and dismissing the keyboard. Set focusable=false on the
PopupProperties so the menu opens without affecting keyboard state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zoom (font size) was lost when going back to the connection list because
TerminalPanes leave the Compose tree, destroying the views. On re-entry
new TerminalSurfaceViews started with the default 14sp.
Store zoom per session in MainViewModel._sessionFontSizes. The
onZoomResize callback saves the new font size, and fontSizeOverride
restores it when the TerminalPane factory recreates the view.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During Connecting, resize() skips the changeWindowDimensions call.
After connect, the PTY has the initial size (80x24) but the
ScreenBuffer was already reflowed to match the surface (42x29).
Added LaunchedEffect that sends resizePty on Connected transition
so the server matches the actual terminal dimensions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, back cycled between sessions when multiple tabs were open.
Now it always switches to the connection list (NavHost) regardless of
session count. Session switching stays in the tab bar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Don't auto-switch to terminal when reopening app from launcher.
Added initialBindComplete flag — first collect emission after service
bind treats all sessions as pre-existing (no auto-switch). Also stop
clearing _activeSessions in onServiceUnbound() to prevent brief
"0 sessions" flash in connection list.
2. Reflow ScreenBuffer when terminal dimensions change. The buffer was
created with 80 cols but the surface reported 42 — SSH server
formatted for 42 but the buffer wrapped at 80, putting content
off-screen. Now reflowResize() + replaceEngine() on every dimension
change ensures buffer matches the actual terminal size.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session tab 3-dot menu: add Rename (dialog with text field) and Theme
(picker with 7 themes) menu items. All 5 actions now restored from the
pre-Compose implementation.
FIX 1: Centered spinner overlay on TerminalPane during Connecting state
with a Cancel button that appears after 3 seconds.
FIX 2: After 3 failed auto-reconnect attempts, session stays in
activeSessions as Disconnected instead of being silently removed.
DisconnectedBar now has a Close button so user decides when to dismiss.
FIX 3: Brighten ANSI banner colors — error 31→91, disconnect 33→93,
reconnect 36→96, SSH debug log 2;90→37, port forward errors 31→91.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The menu was lost when SessionTabBar was rewritten in Compose (Phase 2).
Adds DropdownMenu with Duplicate, Connect via SFTP (SSH/Telnet only),
and Close actions. Chips widened from 120dp to 150dp to fit the icon.
Added MainViewModel.duplicateSession() which connects to the same host
with a new session ID, preserving auth, jump host, and lock settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full ADB test pass on Zebra TC21 covering app launch, SSH connection,
terminal I/O, key sequences, session lifecycle, alternate screen (vim,
htop), scrollback, keepalive, rapid input, keyboard sequences, security
log redaction, app lifecycle, and foreground notification.
Bugs fixed in prior commits:
- FileLogger not writing to file on startup (8faa3c0)
- Double PTY resize sent to SSH server (7c8cdd2)
Added scripts/adb_functional_test.sh — reusable test suite with
pass/fail/skip tracking and manual step prompts. Un-ignored scripts/*.sh
in .gitignore so deployment and test scripts are tracked.
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>
FileLogger.init() was only called when the user manually copied or
cleared the log, leaving the file writer null for the entire session.
Initialize in Application.onCreate() with Downloads dir for debug
builds so file logging works from first launch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 39 instrumented tests pass on TC21 (Android 14) and CT45P (Android 11)
with zero failures. No test fixes needed after single-Activity refactor —
BiometricAuth reflection field was already updated in previous commit.
Combined full suite (lint + unit + instrumented) BUILD SUCCESSFUL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Per-connection theme: MainViewModel resolves SavedConnection.themeName
per session, passes to TerminalPane as themeOverride. When set, overrides
global theme preference. Empty = use global default.
- AndroidManifest: added ACCESS_WIFI_STATE + CHANGE_WIFI_STATE (needed for
per-session WiFi lock in TerminalService)
- MainViewModel: sessionThemes StateFlow tracks per-connection themes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deleted 8 files (3,800+ lines removed):
- TerminalActivity.kt (1,221 lines)
- TerminalConnectionHelper.kt
- TerminalSnippetHelper.kt
- TerminalKeyboardSettingsHelper.kt
- TerminalDisconnectedMode.kt
- TerminalSessionNav.kt
- SftpActivity.kt
- ConnectionManagerActivity.kt
Manifest: removed all three old Activity declarations. Only MainActivity
remains as the launcher Activity.
TerminalService: updated notification intent and import to target
MainActivity instead of deleted ConnectionManagerActivity.
Instrumented test updated: ConnectionManagerLaunchTest now references
MainActivity.
Zero startActivity() calls between screens. Zero volatile static fields.
Zero FLAG_ACTIVITY_REORDER_TO_FRONT hacks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SessionTabBar.kt: pure Compose tab bar matching the terminal aesthetic
(41dp height, 120dp uniform chips, teal accent, rounded corners).
Shows one chip per active session + pinned + button.
MainViewModel: session label resolution — fetches SavedConnection name
from Room for each session, falls back to user@host. Labels cleaned up
when sessions are removed.
MainActivity: Layer 3 wired — SessionTabBar sits above the terminal
surface in a Column when TERMINAL pane is active. + button navigates
to pickHost route.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MainViewModel now auto-switches to TERMINAL pane when a new session
appears in the activeSessions map (Connecting or Connected state).
Also auto-switches away from terminal when the active session is
removed (to another session or back to NAV_HOST).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single Activity replacing CMA/TerminalActivity/SftpActivity. Three-layer
Box: terminal surfaces (Layer 1, always in tree, visibility toggled),
NavHost (Layer 2, AnimatedVisibility when NAV_HOST pane), tab bar (Layer 3,
future step).
MainActivity owns service binding, MainViewModel, biometric lock.
BackHandler: multiple sessions → previous session; single session → NAV_HOST.
Notification intent updated to target MainActivity.
Manifest: MainActivity is now the launcher. Old activities kept temporarily
for backwards compatibility (deleted in step 6).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AndroidView wrapper for the terminal surface. Each session gets its own
TerminalPane; visibility toggled via visible flag (invisible panes stay
in tree at size 0 — no View recreation). Handles screen buffer binding,
PTY resize callbacks, input routing, URL tap, and surface ready events.
Keyboard and quick bar will be added in subsequent steps once the
main Activity architecture is in place.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AppState data class with Pane enum (NAV_HOST, TERMINAL) as the single
source of truth for what the user sees. MainViewModel (@HiltViewModel)
owns AppState, manages pane switching, service binding, connect flow,
session recovery, and session observation. Replaces the scattered state
management across ConnectionManagerActivity and TerminalActivity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single-Activity Compose migration Phase 1. No existing Activities deleted.
ScreenMode enum (HOME/PICKER) controls ConnectionListScreen rendering.
PICKER mode: minimal top bar with close button, no settings/keys/FAB,
tap-to-connect only, no edit/delete/duplicate context menus.
pickHost route added to NavGraph — renders ConnectionListScreen in PICKER
mode. "New session" from TerminalActivity now launches CMA with
START_ROUTE=pickHost via FLAG_ACTIVITY_REORDER_TO_FRONT instead of
calling finish().
SftpScreen: pure Compose rewrite of SftpActivity UI (~530 lines).
Path breadcrumbs, LazyColumn file list, SAF upload/download via
rememberLauncherForActivityResult, transfer progress, dialogs for
new folder/rename/delete/large download confirmation. SFTP session
lifecycle managed via LaunchedEffect (open) + DisposableEffect (close).
sftp/{connectionId} route in NavGraph.
SFTP launch points in TerminalSessionNav now navigate via CMA with
START_ROUTE=sftp/$id. SftpActivity marked @Deprecated (kept in
manifest for Phase 2 removal).
CMA reads START_ROUTE intent extra and navigates via LaunchedEffect
on first composition. TerminalService passed through NavGraph to
SftpScreen for session management.
8 unit tests (ScreenMode enum, Routes constants, sftp path generator).
Lint clean, all existing tests green. AUDIT_LOG.md updated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Top bar: remove SFTP icon (+ menu still has it), show connection alias
instead of user@ip, increase height to 41dp, uniform 120dp chips with
3-dot overflow menu (#BBBBBB matching snippet hint color).
Chip menu: Duplicate, Connect via SFTP (SSH only), Color, Theme, Close.
Color dialog merges rename + color picker (name field + 32-color grid).
Theme picker reuses ThemePickerDialog (same bottom sheet as settings).
Contrasting FG color auto-computed for colored chip backgrounds.
Per-session ephemeral data (custom name, chip color, theme) stored in
SessionChipMeta map, cleared on session close.
SFTP activity gets its own session tab bar showing terminal sessions as
teal chips and active SFTP as amber chip. Tapping a terminal chip uses
FLAG_ACTIVITY_REORDER_TO_FRONT to bring TerminalActivity forward without
destroying SFTP. TerminalActivity.pendingSwitchSessionId + onResume()
handles the session switch. SFTP launch also uses REORDER_TO_FRONT.
New strings (EN/ES/SV): session_duplicate, session_close, session_rename,
session_color, session_theme, session_connect_sftp, session_connect_terminal,
session_rename_title, session_rename_hint, session_color_title,
session_theme_title, session_no_color.
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>
Dual-mode session navigation for TerminalActivity:
- Top bar mode: 36dp tab strip with scrollable session tabs (teal accent),
SFTP tab (amber), + popup menu (New session / Open SFTP / Switch to drawer)
- Drawer mode: 260dp left DrawerLayout with session list (teal dot indicators),
SFTP section, hamburger button on quick bar, footer to switch back
- In-session switching via popup/drawer footer; global default in Settings
- Session switching rebinds TerminalSurfaceView.screenBuffer to target session
New files:
- TerminalSessionNav.kt — tab bar, drawer, session switching, hamburger button
- SftpActivity.kt — stub ("SFTP — coming soon"), registered in manifest
- SessionNavTest.kt — 8 unit tests for NavMode enum and label truncation
Settings:
- Session navigation preference (Top bar / Drawer) in Appearance section
- Show session tab bar toggle (when top bar mode selected)
- sessionNavStyle + showSessionTabBar in TerminalPreferences + SettingsViewModel
Also: rewrote docs/SOCKS5_TEST.md to use adb forward from dev machine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adaptive icons (API 26+) with solid color backgrounds and PNG foregrounds.
Legacy mipmap PNGs for mdpi through xxxhdpi. Design: large W with >_ overlay,
monospace bold, rendered from SVG spec via cairosvg.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Password auth: server-driven flow (try connect first, prompt on auth failure),
IME Done key triggers Connect, masked by default, keyboard focus/show
- Credential fixes: deletePassword when cleared in edit screen, no auto-save
of password in autoSaveConnection (only explicit "Remember" saves)
- Dialog icons: moved from centered body to title bar (setIcon) across all dialogs
- Keyboard: defer terminalView focus until Connected state, show AKB explicitly
after auth retry succeeds
- Password dialog: ADJUST_PAN soft input mode, requestFocus + showSoftInput
- Firebase Analytics + Crashlytics integration (BoM, plugins, .gitignore)
- ADB testing: broadcast receiver enhancements, test script, docs/TESTING.md
- Quick bar: double-tap word jump, CTRL modifier for system keyboard,
key repeat on quick bar, green highlight for active modifiers
- Snippet hint colors lightened (#BBBBBB)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove ExtraKeysBar entirely — quick bar from custom keyboard now serves
both keyboard modes, always visible
- System keyboard mode: quick bar always shown, custom keyboard container
hidden, no longer forced visible on Connected state re-observation
- Selection handle snap-to-content on drop: end handle snaps backward to
last non-whitespace, start handle snaps forward to next content line
- Suppress IME suggestions with VISIBLE_PASSWORD input type
- Context menu: show "New Session" (green) instead of "Connect" when host
has active sessions
- Fix APK date: resolve date at task execution time, not configuration
time, so it's never stale from Gradle config cache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Preserve terminal screen buffer across reconnect for both SSH and Telnet
sessions (was only working for SSH triggerReconnect path)
- Add network awareness to auto-reconnect: wait for connectivity before
counting attempts, reset counter when network returns
- Add ACCESS_NETWORK_STATE permission for ConnectivityManager callbacks
- Fix launcher icon reopening: detect MAIN/LAUNCHER intent in
ConnectionManagerActivity and finish() to reveal existing TerminalActivity
- Fix spurious blank line on resume: only write separator on
Connecting→Connected transition, not on re-observation
- Guard performEditorAction to only send Enter for explicit IME actions
- Add reconnecting_in and waiting_for_network strings (EN/ES/SV)
- Strengthen CLAUDE.md strings.xml rule
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>