Skip to content

Repository files navigation

Threadline

Threadline is an exploratory, transcript-first SSH client for Android. The product idea is that commands should feel like messages and output should feel like responses, while a real terminal remains underneath for interactive work.

Alpha status

Phase 5: Alpha polish is in progress. Its feature slices and owner-device acceptance through alpha.8 are complete. Alpha.5 installed in place but is rejected because a release-shrinker failure prevents SSH connection. Alpha.6 corrects that failure and has passed independent CI, permanent signing, in-place update, and physical acceptance. Alpha.7 adds local drafting while a command runs and retained-session Home navigation. Alpha.8 adds isolated child-Bash execution for script-like commands so a strict-mode failure does not close the persistent SSH shell. The remaining product boundary is invited technical-alpha use sufficient to evaluate whether ten real users can complete small remote tasks for two weeks and provide useful feedback.

The current accepted signed build and source version are both 0.1.0-alpha.8. Alpha.5 is an immutable rejected artifact. On the owner's Galaxy S25 Ultra, an isolated strict-mode failure returned exit 1, preserved the persistent shell's state, and allowed the next persistent command to succeed. Signed builds are shared directly with invited testers. There is no public signed APK or GitHub prerelease while Phase 5 is open. Testers should obtain the APK and its checksum from the owner through the agreed private channel, then follow the technical-alpha guide. Do not distribute alpha.5.

Threadline has no supported production release. Do not use the alpha for privileged or sensitive systems.

Phase 4 security and persistence is implemented. The app opens on a deliberately plain command transcript with a saved multiline composer, streaming command cards, bounded ANSI-aware output, lifecycle status, interactive-terminal suggestions, and one-tap access to the same persistent raw terminal with mobile modifier and navigation keys.

Prototype tour

These captures show the current functional prototype using the repository's local OpenSSH fixture and synthetic test output. They document behavior, not a finished visual design.

Transcript-first command view Same-session raw terminal
A successful synthetic command displayed as a structured Threadline transcript card The same Threadline SSH session displayed as a raw terminal

See the complete prototype screenshot tour for onboarding, connection setup, and saved transcript history.

What the prototype contains

  • Native Kotlin, Jetpack Compose, and Material 3 in one Android application module, plus a plain-JVM SSH integration harness
  • ConnectBot's coroutine SSH library behind a narrow adapter
  • A bundled Conscrypt provider with an Android Ed25519 capability probe and modern ECDSA/RSA-SHA2 compatibility fallback
  • ConnectBot's libvterm-backed Compose terminal component
  • Password and imported OpenSSH private-key authentication, with an explicit option to save keys under Android Keystore-backed AES-GCM encryption and to rename or confirmation-delete saved records
  • Explicit Room-backed save, select, update, copy, and confirmation-delete for non-credential host profiles
  • Explicit confirmation and Room persistence for unknown host keys
  • Default blocking for changed host keys
  • Trusted-server listing with fingerprints and timestamps, confirmation-gated forgetting, and no one-tap changed-key replacement
  • Bounded Room-backed transcript history with a default 20-session retention window, newest-50-turn session snapshots, chunked 65,536-character output tails, confirmation-gated per-session deletion, and clear-all
  • An explicit ephemeral-session option that never hands commands or output to the transcript archive
  • A selectable, bounded diagnostic preview that redacts host fields, usernames, directories, commands, output, credentials, and host-key material by default, with explicit opt-in only for host fields, directories, and recent command text before Android sharing
  • Typed DNS, timeout, refusal, unreachable-network, authentication, key, host-trust, PTY, shell, service, and session failures with non-secret recovery guidance and direct server/credential/settings actions
  • Assertive error announcements, navigable headings, full spoken terminal-key labels, and a connected-session action row proven reachable at 200% system font scale
  • A versioned, one-screen first-run introduction covering transcript and same-session terminal behavior, direct verified connections, credential and transcript-retention boundaries, plus a Help reopen path and blank production connection defaults
  • A repeatable Android/OpenSSH performance runner for styled Unicode volume, long lines, progress rewrites, interruption under sustained output, bounded memory, and post-load recovery
  • Idempotent migration from the dependency spike's private known-host preferences without allowing stale trust to replace a Room record
  • PTY creation, raw ordered output, keyboard input, and resize propagation
  • A foreground service and visible disconnect notification
  • Docker-based OpenSSH fixture with password and generated Ed25519-key auth
  • Unit tests for session transitions, host-key decisions, credential wiping, safe shell quoting, bootstrap generation, and the incremental marker parser
  • Android tests for the exact Ed25519 decode/sign/verify path, selective connection-form retention, Room migration, and Android Keystore tamper detection
  • A bounded, ordered input queue so rapid IME and paste events cannot reorder bytes on the SSH channel
  • A bounded incremental transcript collector for UTF-8, line controls, repeated-carriage-return progress, and ANSI SGR style runs
  • Immutable, session-local command turns with batched streaming updates, live duration, status, exit code, directory, truncation, and approximation state
  • A multiline command composer and neutral command cards with one-shot stop, delayed explicit disconnect, Older/Newer command history with draft restoration, local drafting while a turn runs, copy, edit, rerun, output collapsing, selectable output, confirmed HTTP(S) links, and raw-terminal switching
  • Explicit persistent and isolated execution paths. Isolated commands run in a child Bash process so strict-mode failures cannot terminate the persistent shell, and their mode survives card reruns and saved transcript history
  • One-active-session Home navigation with explicit Return and Disconnect actions; a second connection remains disabled
  • Advisory detection of alternate-screen, cursor-addressing, mouse-tracking, and bracketed-paste control sequences with an explicit same-session terminal handoff
  • A horizontally scrollable terminal key row with one-shot Ctrl and Alt plus Esc, Tab, arrows, Home, End, Page Up, Page Down, and Delete
  • An opt-in plain-JVM smoke test that compiles the production SSH and structured shell code, then proves auth, PTY resize, persistent state, multiline input, lifecycle markers, current directory, and exit status against the fixture

Requirements

  • JDK 17
  • Android SDK Platform 37
  • Docker with the Compose plugin
  • OpenSSH ssh-keygen on the development host

Build

./gradlew test
./gradlew lint
./gradlew assembleDebug

Standard debug builds install as Threadline Debug under io.github.r055le.threadline.debug. The permanent release application ID is io.github.r055le.threadline; release and debug builds therefore keep separate Android data and Keystore records.

With an emulator or device running:

./gradlew connectedDebugAndroidTest

The debug APK is written to app/build/outputs/apk/debug/app-debug.apk.

Release signing is deliberately local and secret-free in Git. Once the current main commit passes CI, ./scripts/sign-latest-alpha.sh downloads and verifies its exact candidate before prompting locally for the signing passwords. See the release-signing guide to create and back up the long-lived key and verify the signed APK. The technical-alpha guide defines installation, update, test, feedback, diagnostics, and metrics boundaries.

Windows/WSL setup and APK-install notes are in the Android development guide.

Run the SSH fixture

cd fixtures/openssh
cp .env.example .env
# Set a local-only password in .env.
./start.sh

The documented test username is threadline. The password is whatever you put in the ignored .env file. The startup script also creates an unencrypted fixture-only Ed25519 key at fixtures/openssh/.state/client_ed25519.

See the fixture guide for host fingerprint, key-auth, adapter-smoke, output, shutdown, and changed-key test commands.

Connect from an Android emulator

Use:

  • Display name: Local fixture
  • Host: 10.0.2.2
  • Port: 2222 (or THREADLINE_TEST_PORT from .env)
  • Username: threadline
  • Password: the local value from .env

The first connection pauses at the server fingerprint. Compare it with:

cd fixtures/openssh
docker compose exec openssh \
  ssh-keygen -lf /var/lib/threadline-ssh/ssh_host_ed25519_key.pub

For key auth, copy the generated private key to the emulator, choose Private key in the app, and select it through Android's file picker. Check Save encrypted on this device before connecting to retain an encrypted copy. A saved key appears by name and public fingerprint on later connections; its passphrase, if any, must still be entered for each connection and is never saved. Saved-key labels can be renamed without decrypting or re-encrypting the credential. Delete shows the key fingerprint for confirmation, removes only that encrypted local record, and does not revoke its public key on a server.

Connection details can be saved explicitly as a host profile. A profile stores only its name, hostname, port, username, stable ID, and timestamps. Selecting one fills those fields and clears any password, private-key passphrase, saved key choice, or pending file import. Authentication mode and credentials are not linked to profiles. Edit the fields and choose Update profile, or choose Use as new to preserve the fields while creating a separate profile.

Accepted host keys appear under Trusted servers with their algorithm, fingerprint, first-trusted time, and last-verified time. Forget removes only that trust decision after confirmation. A changed key is still blocked without prompting: deliberate replacement requires forgetting the exact old record, reconnecting, verifying the newly presented fingerprint through a trusted channel, and explicitly accepting it as unknown.

Completed transcript sessions are retained locally by default. Saved transcripts opens selectable plain-text history, including command status, exit code, and truncation notices. Threadline retains at most 20 sessions, the newest 50 turns in each saved session, the first 16,384 characters of each command, and the last 65,536 characters of plain output per turn. Saved output does not retain ANSI styling and does not create active links. Per-session delete and clear-all are both confirmation-gated; they are logical SQLite deletion, not guaranteed forensic erasure.

Transcript commands and output may themselves contain sensitive values. The history database is local and excluded from backup and device transfer, but it is not encrypted. Select Ephemeral session before connecting when commands and output should not be archived after disconnect. A finalized saved session survives database reopen; an abrupt process kill can still lose the current unfinished session because archive writes happen at session finalization.

Architecture

Compose host/terminal UI
        │ events + immutable StateFlow
        ▼
SessionManager ─────────────── Foreground SshSessionService
        │
        ├── StrictHostKeyGate ── Room known-host store
        ├── Imported-key store ─ Room ciphertext + Android Keystore AES key
        ├── Host-profile store ─ Room connection metadata
        ├── Transcript store ─── Room sessions, turns, and output chunks
        ├── SshClientAdapter ─── ConnectBot sshlib
        └── TerminalBridge ───── ConnectBot termlib/libvterm

TerminalBridge is process-owned rather than composable-owned. It receives the PTY byte stream even while the Activity is absent. The foreground service owns the live-session policy, and its destruction cancels or closes session jobs. Passwords and private-key passphrases are not persisted. A private key is memory-only unless the user explicitly saves it; saved keys are persisted only as authenticated ciphertext plus non-secret format and public-fingerprint metadata.

The dependency choice and its open questions are recorded in ADR 0001. The Android connection failure, isolation method, root cause, fix, and remaining risks are recorded in the July 2026 investigation.

Project status

Phases 0 through 4 are complete. Phase 5: Alpha polish is in progress. Its accessibility/error, large-output performance, Samsung physical-validation, and basic-onboarding slices are implemented. The installed-update preservation check is complete. Direct invited sharing is the selected alpha distribution path, with no public prerelease while Phase 5 remains open. Sufficient technical-alpha use is the remaining boundary. Additional device and OEM coverage, including Pixel, is opportunistic alpha evidence rather than a separate release gate.

Alpha packaging preparation now defines the permanent release/debug identities, explicit alpha versioning, interactive off-repository key creation, local align/sign/verify/checksum tooling, tester checklist, and feedback form. The owner-created 0.1.0-alpha.1 APK established the certificate record and passed installation, but a release-only R8/JNI crash on its first physical connection attempt rejected that artifact. Permanent-key alpha.2 fixed Connect, installed over alpha.1 with data preserved, authenticated by password, and completed a structured command before raw-terminal opening exposed a second native field contract omitted from the shrinker rule. Alpha.2 is therefore also rejected. The permanent-key 0.1.0-alpha.3 artifact preserves and verifies both native field contracts, matches the established signing lineage, and has now passed the corrected physical release path on the Galaxy S25 Ultra. A running ping survived repeated transcript/terminal switching and rotation in each view. The release/JNI blocker is closed. Two encrypted off-machine signing-key backups now exist with separately held decryption material, and independent restores verified both copies against the established key identity. Alpha.3 imported-key authentication and default diagnostic checks now pass on the physical release artifact. Public CI now produces source-identified unsigned candidates while the permanent signing key remains local. The permanent-key 0.1.0-alpha.4 artifact installed over alpha.3 in place, retained onboarding, profiles, trusted hosts, transcripts, settings, and the encrypted saved key, then authenticated with that key without re-import. The permanent-key alpha.5 artifact was selected through the resumable local signing path, verified against the established certificate, and installed over the existing physical release in place. Its first connection attempt exposed a release-only cbssh Ed25519 provider failure caused by R8 relocation. Alpha.5 is rejected; an isolated minified probe reproduces the failure and proves the narrow keep-rule correction intended for alpha.6. The alpha.6 source applies that narrow rule and makes the release gate verify the exact provider class names in both the R8 mapping and assembled DEX. Its isolated minified password SSH proof passes. Independent CI then produced the exact candidate signed with the permanent key. Alpha.6 installed over alpha.5 with state intact and passed password and retained imported-key authentication, Diagnostics, structured commands, and same-session raw-terminal acceptance on the Galaxy S25 Ultra. Invited technical-alpha use can resume.

Use these records according to their purpose:

License

Apache License 2.0. See LICENSE.

About

An exploratory transcript-first SSH client for Android

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages