Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,23 @@ 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. Alpha.9 is the current source
candidate and keeps the followed transcript tail visible when the software
keyboard changes the viewport. 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.
failure does not close the persistent SSH shell. Alpha.9 attempted to keep the
followed transcript tail visible when the software keyboard changed the
viewport, but the signed build failed that check on the owner device. Alpha.10
is the current source candidate and makes the production activity resize for
the keyboard. 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 is `0.1.0-alpha.8`; the current source
candidate is `0.1.0-alpha.9`. Alpha.5 is an immutable rejected artifact.
Alpha.9 owner-device acceptance is pending. Signed builds are shared directly
with invited testers.
candidate is `0.1.0-alpha.10`. Alpha.5 and alpha.9 are immutable rejected
artifacts. Alpha.10 owner-device acceptance is pending. 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](docs/alpha-testing.md).
Do not distribute alpha.5.
Do not distribute alpha.5 or alpha.9.

Threadline has no supported production release. Do not use the alpha for
privileged or sensitive systems.
Expand Down
59 changes: 43 additions & 16 deletions app/src/androidTest/java/dev/threadline/TranscriptScreenTest.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package dev.threadline

import android.content.ComponentName
import android.os.SystemClock
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.lazy.LazyListState
Expand Down Expand Up @@ -989,25 +991,46 @@ class TranscriptScreenTest {
}
}

@Suppress("DEPRECATION")
@Test
fun mainActivityRequestsResizeForIme() {
val activityInfo = composeRule.activity.packageManager.getActivityInfo(
ComponentName(composeRule.activity, MainActivity::class.java),
0,
)

assertEquals(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE,
activityInfo.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST,
)
}

@Test
fun transcriptKeepsLatestTurnVisibleWhenImeInsetsArrive() {
assertSubmittedTurnVisibleWithIme(
initialTurns = (0 until 6).map { index ->
turn(
id = "command-$index",
command = "printf command-$index",
status = CommandStatus.SUCCEEDED,
output = "result-$index",
)
},
)
}

@Test
fun transcriptKeepsFirstTurnVisibleWhenImeInsetsArrive() {
assertSubmittedTurnVisibleWithIme(initialTurns = emptyList())
}

private fun assertSubmittedTurnVisibleWithIme(initialTurns: List<CommandTurn>) {
composeRule.runOnIdle {
composeRule.activity.enableEdgeToEdge()
}
lateinit var composeView: View
val latestTurnId = "ime-command"
val transcript = mutableStateOf(
CommandTranscriptState(
turns = (0 until 6).map { index ->
turn(
id = "command-$index",
command = "printf command-$index",
status = CommandStatus.SUCCEEDED,
output = "result-$index",
)
},
),
)
val transcript = mutableStateOf(CommandTranscriptState(turns = initialTurns))
composeRule.setContent {
MaterialTheme {
ConnectedSessionScreen(
Expand All @@ -1030,8 +1053,10 @@ class TranscriptScreenTest {
)
}
}
composeRule.onNodeWithTag(TranscriptTags.output("command-5"))
.assertIsDisplayed()
initialTurns.lastOrNull()?.let { lastTurn ->
composeRule.onNodeWithTag(TranscriptTags.output(lastTurn.id.value))
.assertIsDisplayed()
}
composeRule.onNodeWithTag(TranscriptTags.COMPOSER)
.performClick()
.assertIsFocused()
Expand Down Expand Up @@ -1060,8 +1085,10 @@ class TranscriptScreenTest {
"Composer bottom $composerBottom exceeded IME top $visibleBottom",
visibleBottom != null && composerBottom <= visibleBottom,
)
composeRule.onNodeWithTag(TranscriptTags.output("command-5"))
.assertIsDisplayed()
initialTurns.lastOrNull()?.let { lastTurn ->
composeRule.onNodeWithTag(TranscriptTags.output(lastTurn.id.value))
.assertIsDisplayed()
}
composeRule.onNodeWithTag(TranscriptTags.COMPOSER)
.performTextInput("printf latest")
composeRule.onNodeWithTag(TranscriptTags.SEND).performClick()
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
android:usesCleartextTraffic="false">
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
23 changes: 14 additions & 9 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ visual case.

## Transcript viewport with the software keyboard

**Status:** Implemented in source with emulator regression coverage; physical
**Status:** The signed alpha.9 build failed physical validation. An alpha.10
correction is implemented in source with emulator regression coverage; physical
owner-device validation remains pending.

On the Galaxy S25 Ultra, focusing the transcript composer and sending commands
Expand All @@ -61,14 +62,18 @@ user had to scroll back up to recover the new cards and output. Connection,
execution, and data remained intact. The recovery scrolling is repeated
friction in the primary interaction path.

The transcript followed new output but did not re-anchor the followed tail when
the software keyboard changed the list viewport height. Tail-following now
reacts to viewport resizes while preserving the IME padding that keeps the
composer above the keyboard. An instrumented emulator regression uses the real
Gboard window and the full connected-session layout. It failed against the old
layout and verifies that the composer, previous result, and newly submitted
result all remain visible after the fix. The raw terminal keeps its separate
IME handling.
Alpha.9 made tail-following react to list viewport resizes while preserving the
IME padding that keeps the composer above the keyboard. Its first physical
check exposed a second boundary: the production activity left soft-input
adjustment unspecified, so the Galaxy S25 Ultra panned the whole window instead
of resizing the viewport. A completed first turn existed but remained outside
the visible window until the keyboard closed. The session and command remained
functional.

Alpha.10 explicitly requests resize behavior for `MainActivity`. An
instrumented contract checks the production manifest, and the real-Gboard
connected-session regression now starts from an empty transcript and submits
the first turn. The raw terminal keeps its separate IME handling.

Owner-device acceptance should repeat focus, submission, running output,
completion, keyboard dismissal, rotation, and large-font checks. Existing
Expand Down
30 changes: 21 additions & 9 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,27 @@ The next persistent command returned the original marker and `/tmp`, with exit
0 and without reconnecting. This closes alpha.8 owner-device acceptance. See the
[alpha.8 isolated-execution investigation](investigations/2026-08-17-alpha8-isolated-execution-acceptance.md).

The current source candidate is `0.1.0-alpha.9` (`10009`). It keeps a followed
transcript tail anchored when the software keyboard changes the list viewport
height. A connected-session emulator regression using the real Gboard window
reproduces the prior loss of the newest useful card, then verifies that the
composer, previous result, and a newly submitted result remain visible after
the fix. CI runs the regression on an explicit Pixel 6 AVD profile. Deliberate
user scrolling still disables tail-following, and the raw terminal path is
unchanged. Exact merged-main CI, permanent signing, update installation, and
owner-device acceptance remain pending.
The exact merged-main alpha.9 candidate passed CI, was signed with the permanent
key, and installed on the Galaxy S25 Ultra. Its certificate, v2/v3 signatures,
package identity, version, checksum, and 16 KiB alignment verify. Physical
testing then rejected it: after Gboard opened over an empty transcript, a
completed first turn existed but remained above the visible window until the
keyboard closed. The session and command remained functional.

The failure exposed a production boundary missing from the emulator regression.
`MainActivity` enabled edge-to-edge layout but left its soft-input adjustment
unspecified. The Pixel emulator resized the generic test activity, while the
Samsung device panned the production window. The viewport-follow effect could
only react to the resize path.

The current source candidate is `0.1.0-alpha.10` (`10010`). It explicitly makes
the production activity resize for the software keyboard, adds an instrumented
contract for that manifest setting, and starts the Gboard regression from the
same empty-transcript first-command sequence. Deliberate user scrolling still
disables tail-following, and the raw terminal path is unchanged. Exact
merged-main CI, permanent signing, update installation, and owner-device
acceptance remain pending. See the
[alpha.9 IME physical rejection](investigations/2026-08-24-alpha9-ime-physical-rejection.md).

## Remaining Phase 5 boundaries

Expand Down
16 changes: 16 additions & 0 deletions docs/development/release-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ was selected through `scripts/sign-latest-alpha.sh`. Android installed it on
the Galaxy S25 Ultra, where isolated strict failure, state containment, and
persistent-command recovery passed against the disposable fixture.

Permanent-key alpha.9 uses the same certificate. Its verified APK SHA-256 is:

```text
446b9c048cf720bd35bc2721f43e3ac49ba22660b116fda00da3342fa7862e7d
```

The artifact identifies as `io.github.r055le.threadline`, version
`0.1.0-alpha.9` (`10009`), verifies with APK signature schemes v2 and v3, and
is 16 KiB page aligned. Its exact merged-main candidate passed public CI and
was selected through `scripts/sign-latest-alpha.sh`. Android installed it on
the Galaxy S25 Ultra, but the followed transcript tail still moved outside the
visible window when Gboard opened. Alpha.9 is rejected and must not be
distributed or overwritten. Preserve it as immutable evidence and advance the
corrected build to alpha.10. See the
[alpha.9 IME physical rejection](../investigations/2026-08-24-alpha9-ime-physical-rejection.md).

## Signing-key boundary

The long-lived release keystore and its passwords must never enter this
Expand Down
59 changes: 59 additions & 0 deletions docs/investigations/2026-08-24-alpha9-ime-physical-rejection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Alpha.9 IME physical rejection

Date: 2026-08-24

## Result

The permanent-key alpha.9 build is rejected. It installed successfully and the
structured shell remained functional, but it did not keep the newest transcript
card visible while Gboard was open on the Galaxy S25 Ultra.

## Physical evidence

The owner-device check started with an empty transcript:

1. Focusing the composer opened Gboard.
2. Submitting a short command completed the turn and enabled command history.
3. The completed card remained above the visible window while the keyboard was
open.
4. Closing the keyboard immediately revealed the successful card.

This isolates the failure to window and transcript presentation. The command,
captured output, SSH session, and recovery path remained intact.

## Cause

Alpha.9 made tail-following react when the `LazyColumn` viewport height changed.
The production `MainActivity` also enables edge-to-edge layout, but its manifest
left `android:windowSoftInputMode` unspecified. The Pixel emulator resized the
generic activity used by the regression. The Samsung device instead panned the
production window to expose the focused composer, so the list viewport did not
provide the resize signal the alpha.9 effect expected.

Android's current
[edge-to-edge setup guidance](https://developer.android.com/develop/ui/compose/system/setup-e2e)
requires an activity using the software keyboard to declare `adjustResize` so
the app receives IME insets for layout adjustment.

## Correction boundary

Alpha.10 declares `adjustResize` on production `MainActivity`. An instrumented
test checks that exact manifest contract. The real-Gboard UI regression now
starts with an empty transcript and submits the first command, matching the
physical failure sequence.

Before the manifest change, the contract failed with adjustment mode `0`
instead of `16`. After the change, the manifest contract and first-command IME
regression pass on the Pixel 6 API 35 emulator. The full emulator run completed
78 tests with three expected fixture-dependent skips. JVM tests, lint, debug and
minified release assembly, the signing-helper tests, and the release shrinker
contract also pass.

## Still required

- Pass the merged-main release gate.
- Sign the exact merged-main alpha.10 candidate with the permanent key.
- Install alpha.10 over alpha.9 and preserve existing app data.
- Repeat the owner-device keyboard sequence, including running output,
completion, keyboard dismissal, rotation, large font, and deliberate manual
scrolling.
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
android.useAndroidX=true
android.nonTransitiveRClass=true
threadline.releaseApplicationId=io.github.r055le.threadline
threadline.versionCode=10009
threadline.versionName=0.1.0-alpha.9
threadline.versionCode=10010
threadline.versionName=0.1.0-alpha.10