feat(android): wire the dead code, render the git panel, make the token gate real
Four agents' worth of :app wiring. Everything here existed as tested code with
zero production call sites, or as a screen rendering a shape the server stopped
sending months ago.
Dead code that shipped as features. All three were fully implemented and
unit-tested while being reachable from nothing:
- Session-list preview thumbnails. SessionsHome called SessionListScreen
without the `thumbnails` argument, so the `= null` default won and every
row rendered a placeholder forever — on the home session chooser, whose
entire purpose is seeing what each session is doing.
- Quick-reply chips and their palette editor.
- PointerContextMenu (large-screen secondary click).
ThumbnailPipeline's formal cross-review had been explicitly skipped when it was
written — PROGRESS_ANDROID.md said so in as many words, and it is concurrency
code, so nothing had ever reviewed OR executed it. It has now been reviewed and
kept: the LruCache key, the fair Semaphore(2), the Mutex-guarded in-flight
dedup and the NonCancellable in-flight release are all sound, and a retained
map entry would have poisoned a key forever.
The v0.6 project-detail git panel. Android rendered the pre-w6 shape: one bare
dirty dot, a bare hash+subject commit row, and a worktree heading that renamed
itself to "Branch" at n=1 — the exact behaviour G5 removed. Now the sync band,
unpushed marking with a single upstream boundary, and "Worktrees (n)". The rule
that drives it is enforced, not documented: exactly one state may render green,
and no-upstream is not that state.
Remote approvals are no longer blind. status.preview reaches GateBanner and
PlanGateSheet, so the user can see the command or diff they are approving
instead of a tool name. Absent preview stays an ordinary gate, not an error.
The WEBTERM_TOKEN gate is no longer inert. NetworkModule was calling
OkHttpClientFactory.create(identityProvider) with no cookieJar, so the shared
client ran on CookieJar.NO_COOKIES and none of the cookie plumbing did
anything. It now installs the real AuthCookieJar — one client, so the cookie
rides the WS upgrade too — backed by the Tink-sealed store, and binds
InMemoryAuthCookieStore if a cipher cannot be constructed rather than ever
persisting a shell credential in the clear. The persister runs on an IO scope
because OkHttp calls a CookieJar synchronously and must never block on
DataStore or Tink.
And the user-facing half that made the gate unusable: a token-gated host
answers the pairing probe's unauthenticated GET with 401, which the frozen
taxonomy could only report as "this port is running something else" — so such
a host was impossible to pair AND the copy was misleading. Pairing now routes
to a token prompt and resumes through the same cert-gate choke point. The
token is a parameter, never a field and never part of any UI state.
Docs corrected rather than left aspirational: the plan asserted a cleartext
allowlist "permitted only for private LAN/Tailscale CIDRs", which the platform
cannot express at all — the network-security-config format has no netmask or
prefix syntax. PROGRESS_ANDROID.md now opens with what "COMPLETE" actually
meant, and PROGRESS_LOG.md records the whole pass including what is still
undone: A34/A35 have no code, and device QA remains ~0%.
Verified: ./gradlew test :app:assembleDebug :app:testDebugUnitTest koverVerify
-> BUILD SUCCESSFUL, 893 JVM tests, 0 failures (baseline was 617). The :app
run was re-measured with outputs deleted and --no-build-cache, and the APK
re-packaged, so Hilt graph validity is an observation rather than an
up-to-date claim.
This commit is contained in:
@@ -372,6 +372,80 @@ warmUp off-Main; SessionActivityBridge drives LastSessionStore. Hilt graph valid
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ CORRECTION (2026-07-30): "COMPLETE" below meant COMPILES, not WORKS
|
||||
|
||||
The section that follows claimed all 36 tasks had landed. That was true at the level it was measured —
|
||||
the modules built, the APK assembled and ~484 JVM tests were green — but NOTHING had ever run on an
|
||||
Android device or emulator, and the first thing that would have happened on one is a crash. An audit on
|
||||
2026-07-30 found, and a repair pass fixed, the following. Read this before trusting anything below.
|
||||
|
||||
**Three defects made the app unusable on real hardware.** Each was invisible to the JVM suite for the
|
||||
same structural reason: no JVM test instantiates an Android `View`.
|
||||
|
||||
1. **Every key press crashed.** `RemoteTerminalView` bound the forked emulator but never installed a
|
||||
`TerminalViewClient`. The pinned Termux `TerminalView` dereferences `mClient` with no null guard on
|
||||
the first line of the paths a user actually hits (`onKeyDown` offset 82, `onCreateInputConnection`
|
||||
offset 0, `onKeyUp`, `onKeyPreIme`). The KDoc claimed "A17 sets it" — A17 never did; only the
|
||||
`HardwareKeyRouter` and the KeyBar shipped, and the KeyBar works precisely because it bypasses the
|
||||
view. Installing a client is not sufficient: returning false continues into `mTermSession.write()`
|
||||
at offset 150, and `mTermSession` is null forever (`TerminalSession` is final and forking a process
|
||||
is exactly what this app must not do, §6.1). Fixed by a client that consumes the key itself, with
|
||||
`KeyRouting` deliberately having NO defer-to-stock branch, so the crash is unrepresentable rather
|
||||
than merely avoided. `KeyHandler` remains the authority for the key tables, so DECCKM still emits
|
||||
`ESC O A`.
|
||||
2. **No `resize` was ever sent.** `RemoteTerminalSession.updateSize` had zero call sites and the stock
|
||||
fallback is dead (`TerminalView.updateSize` early-returns without a session), so the PTY stayed at
|
||||
the server's 80x24 and every full-screen TUI rendered into the wrong box. `TerminalResizeDriver`
|
||||
now drives it from real cell metrics, read via the PUBLIC `TerminalRenderer.getFontWidth()` /
|
||||
`getFontLineSpacing()` accessors — no reflection (silently breaks under R8) and no re-measured
|
||||
`Paint` (drifts from what the renderer actually draws, the §R5 risk).
|
||||
3. **Bare-LAN connections were impossible.** `network_security_config.xml` was a self-labelled STUB
|
||||
denying all cleartext, while `HostEndpoint` accepts `http` and derives `ws://`. Its own comment
|
||||
promised a CIDR allowlist the platform cannot express. See plan §6.9 for the corrected posture.
|
||||
|
||||
**Adversarial review then found three more, and got one wrong:**
|
||||
|
||||
- **Swipe-scrolling inside an alternate-screen TUI still crashed.** The touch path bypasses
|
||||
`TerminalViewClient` entirely: `doScroll` converts a scroll to `handleKeyCode` whenever the
|
||||
alternate buffer is active, which dereferences `mTermSession`. Claude Code IS an alternate-screen
|
||||
TUI, so this was a crash during ordinary use on the app's main screen. `TerminalScrollGesture` now
|
||||
claims the vertical drag first and reproduces all three `doScroll` branches, closing the fling
|
||||
runnable and `onGenericMotionEvent` with it.
|
||||
- **`autofill()` dereferences `mTermSession` unguarded** while the view advertises itself
|
||||
autofillable, so any password manager crashed the app. The subtree is excluded from the autofill
|
||||
structure.
|
||||
- The review also demanded stock text selection be RESTORED after the focus rework made it
|
||||
unreachable. It cannot be: the ActionMode's own Copy and Paste handlers dereference the absent
|
||||
session (`TextSelectionCursorController$1.onActionItemClicked` offsets 89-100 / 126-136), so a
|
||||
reachable selection UI has an NPE on its Copy button. **Long press stays consumed and copy-out is
|
||||
a recorded FEATURE GAP** — a first-party selection layer over `TerminalBuffer` + `ClipboardManager`
|
||||
is the real fix. This is the one place the reviewer was wrong and the builder right.
|
||||
|
||||
**Silently-lost server data, now decoded:** the W2 `queue` frame had no `ServerMessageType` member so
|
||||
every one was dropped as undecodable (the "N queued" badge could never appear), and W1
|
||||
`status.preview` was not modelled at all, which made remote approvals **BLIND** — the user was asked to
|
||||
approve a `Bash` call without seeing the command. Both now decode, with the rule that a broken preview
|
||||
never costs the frame (`pending` is what makes the gate appear).
|
||||
|
||||
**Dead code that shipped as features:** `ThumbnailPipeline` (session-list previews), `QuickReply`
|
||||
(chips + palette) and `PointerContextMenu` were fully implemented and unit-tested with ZERO production
|
||||
call sites. All three are now wired. `ThumbnailPipeline`'s formal cross-review — explicitly skipped at
|
||||
the time, as the AW3 entry below admits — has finally been run.
|
||||
|
||||
**Credential handling:** the `WEBTERM_TOKEN` cookie is now carried on REST and the WS upgrade, sealed
|
||||
with Tink AEAD under an AndroidKeyStore key, **fail-closed** (a seal failure persists nothing rather
|
||||
than falling back to plaintext), and `allowBackup=false` keeps a 30-day shell credential out of Google
|
||||
Drive and D2D transfer.
|
||||
|
||||
**Still not done — do not read the section below as saying otherwise:**
|
||||
- **A34 / A35 have no code.** There is no `:app` androidTest source set and no benchmark module; the
|
||||
AW6 entry marking them `[x]` reflects them being converted into checklist bullets, not implemented.
|
||||
- **Device QA remains ~0%.** One emulator smoke screenshot of the pairing screen is the only on-device
|
||||
evidence in the repo. Every fix above is JVM-verified only.
|
||||
- Release signing, versioning and minification; the S2 FCM real-handset spike; copy-out via selection.
|
||||
|
||||
---
|
||||
|
||||
## ✅ ANDROID CLIENT COMPLETE — all 36 plan tasks (A1–A36 + S1) landed
|
||||
|
||||
Modules: `:wire-protocol :session-core :api-client :client-tls :test-support :transport-okhttp` (pure
|
||||
|
||||
Reference in New Issue
Block a user