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
3 changes: 3 additions & 0 deletions PROJECT_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,9 @@ When ready:
7. Complete the card when the end marker arrives.

Only one transcript command may be active per shell session in MVP.
While that command runs, the composer remains editable as a local draft, but
Send stays disabled until the structured shell returns to ready. Drafting does
not queue or automatically execute a command.

### 10.2 Interactive input

Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ 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.6`; `0.1.0-alpha.5` is an
immutable rejected artifact. Signed builds are shared directly with invited
testers.
immutable rejected artifact. The current source candidate is
`0.1.0-alpha.7`. 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
Expand Down Expand Up @@ -104,8 +104,9 @@ connection setup, and saved transcript history.
state
- A multiline command composer and neutral command cards with one-shot stop,
delayed explicit disconnect, Older/Newer command history with draft
restoration, copy, edit, rerun, output collapsing, selectable output,
confirmed HTTP(S) links, and raw-terminal switching
restoration, local drafting while a turn runs, copy, edit, rerun, output
collapsing, selectable output, confirmed HTTP(S) links, and raw-terminal
switching
- Advisory detection of alternate-screen, cursor-addressing, mouse-tracking,
and bracketed-paste control sequences with an explicit same-session terminal
handoff
Expand Down
60 changes: 60 additions & 0 deletions app/src/androidTest/java/dev/threadline/TranscriptScreenTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import dev.threadline.core.shell.ActiveCommand
import dev.threadline.core.shell.CommandId
import dev.threadline.core.shell.CommandSubmissionRejection
import dev.threadline.core.shell.CommandSubmissionResult
import dev.threadline.core.shell.CompletedCommand
import dev.threadline.core.shell.LifecyclePhase
import dev.threadline.core.shell.StructuredShellState
import dev.threadline.core.terminal.TerminalKey
Expand Down Expand Up @@ -179,6 +180,65 @@ class TranscriptScreenTest {
assertComposerText("printf one\nprintf two")
}

@Test
fun runningCommandAllowsDraftingAcrossTerminalAndStateRestoration() {
val restorationTester = StateRestorationTester(composeRule)
val structuredShell = mutableStateOf<StructuredShellState>(runningShell())
var submitted: String? = null
restorationTester.setContent {
MaterialTheme {
ConnectedSessionScreen(
displayName = "Draft test session",
structuredShell = structuredShell.value,
transcript = CommandTranscriptState(),
onSubmit = { command ->
submitted = command
CommandSubmissionResult.Accepted(CommandId("next-command"))
},
onControlC = {},
onDisconnect = {},
rawTerminal = { modifier ->
Text("Raw draft test surface", modifier = modifier)
},
)
}
}

composeRule.onNodeWithTag(TranscriptTags.COMPOSER)
.assertIsEnabled()
.performTextInput("printf next")
composeRule.onNodeWithTag(TranscriptTags.SEND).assertIsNotEnabled()

composeRule.onNodeWithTag(TranscriptTags.MODE_SWITCH).performClick()
composeRule.onNodeWithText("Raw draft test surface").assertIsDisplayed()
composeRule.onNodeWithTag(TranscriptTags.MODE_SWITCH).performClick()
assertComposerText("printf next")

restorationTester.emulateSavedInstanceStateRestore()
assertComposerText("printf next")
composeRule.onNodeWithTag(TranscriptTags.SEND).assertIsNotEnabled()

composeRule.runOnIdle {
structuredShell.value = StructuredShellState.Ready(
currentDirectory = "/tmp",
lastCommand = CompletedCommand(
id = CommandId("command-42"),
command = "test command",
directoryAtStart = "/tmp",
currentDirectory = "/tmp",
exitStatus = 1,
),
)
}
composeRule.onNodeWithTag(TranscriptTags.SEND)
.assertIsEnabled()
.performClick()
composeRule.runOnIdle {
assertEquals("printf next", submitted)
}
assertComposerText("")
}

@Test
fun acceptedCardRerunResetsHistoryNavigationWithoutClearingComposer() {
var submitted: String? = null
Expand Down
29 changes: 17 additions & 12 deletions app/src/main/java/dev/threadline/TranscriptScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand Down Expand Up @@ -131,6 +132,7 @@ internal fun ConnectedSessionScreen(
rawTerminal: @Composable (Modifier) -> Unit = { RawTerminal(it) },
) {
var rawModeRequested by rememberSaveable { mutableStateOf(false) }
val transcriptStateHolder = rememberSaveableStateHolder()
val rawModeRequired = structuredShell is StructuredShellState.Unavailable
val showingRawTerminal = rawModeRequested

Expand Down Expand Up @@ -193,17 +195,19 @@ internal fun ConnectedSessionScreen(
.fillMaxSize(),
)
} else {
TranscriptSurface(
structuredShell = structuredShell,
transcript = transcript,
onSubmit = onSubmit,
onStop = onControlC,
onDisconnect = onDisconnect,
onOpenTerminal = { rawModeRequested = true },
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
)
transcriptStateHolder.SaveableStateProvider("transcript") {
TranscriptSurface(
structuredShell = structuredShell,
transcript = transcript,
onSubmit = onSubmit,
onStop = onControlC,
onDisconnect = onDisconnect,
onOpenTerminal = { rawModeRequested = true },
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
)
}
}
}
}
Expand Down Expand Up @@ -458,7 +462,8 @@ internal fun TranscriptSurface(
label = { Text("Command") },
minLines = 1,
maxLines = 5,
enabled = structuredShell is StructuredShellState.Ready,
enabled = structuredShell is StructuredShellState.Ready ||
structuredShell is StructuredShellState.Running,
modifier = Modifier
.weight(1f)
.testTag(TranscriptTags.COMPOSER),
Expand Down
32 changes: 16 additions & 16 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,22 @@ Portrait remains the primary phone layout.

## Drafting and queued commands while a turn runs

**Status:** Deferred interaction design; not a Phase 5 interruption blocker.

The transcript composer is currently disabled whenever the structured shell is
not ready. Physical testing showed that interrupt and recovery are fast, but it
also made the user wait before typing the likely follow-up command.

Treat two possible improvements separately:

1. Allow editing a local draft while a command is running, while keeping Send
unavailable until the shell is ready. Preserve the draft through stopping,
failure, raw-mode switching, rotation, and saved-state restoration.
2. Consider an explicit command queue only as a larger feature. It must define
ordering, visibility, reordering or removal, behavior after failure or
interruption, disconnect handling, and whether queued content is persisted.
Never send a queued command merely because the shell returned to readiness
unless the UI made that execution contract unambiguous.
**Status:** Local drafting implemented in the alpha.7 source; command queue
deferred.

Physical testing showed that interrupt and recovery are fast, but disabling the
composer during a running turn made the user wait before typing the likely
follow-up command. The alpha.7 source keeps the composer editable while a turn
runs, preserves the local draft through stopping, failure, raw-mode switching,
rotation, and saved-state restoration, and enables Send only when the shell is
ready again.

The implemented draft behavior remains separate from an explicit command queue.
That queue remains a larger feature and must define ordering,
visibility, reordering or removal, behavior after failure or interruption,
disconnect handling, and whether queued content is persisted. Never send a
queued command merely because the shell returned to readiness unless the UI
made that execution contract unambiguous.

## Opt-in saved password authentication

Expand Down
9 changes: 8 additions & 1 deletion docs/STATUS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Threadline current status

Updated: 2026-08-10
Updated: 2026-08-11

This is the canonical execution-status page. `PROJECT_SPEC.md` remains the normative product and
technical specification. Dated investigations are historical evidence for the boundary they
Expand Down Expand Up @@ -104,6 +104,13 @@ terminal correctly remained terminal-only rather than creating transcript
cards. See the
[alpha.6 Ed25519 shrinker correction](investigations/2026-08-10-alpha6-ed25519-shrinker-correction.md).

Product work continues while invited alpha use is gathered. The current source
candidate is `0.1.0-alpha.7` (`10007`). Its first slice keeps the structured
composer editable while a command runs, preserves that local draft through
same-session terminal switching and Android saved-state restoration, and keeps
Send disabled until the shell returns to ready. It does not queue or
automatically execute commands.

## Remaining Phase 5 boundaries

- Technical-alpha use sufficient to evaluate the Phase 5 exit criterion.
Expand Down
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=10006
threadline.versionName=0.1.0-alpha.6
threadline.versionCode=10007
threadline.versionName=0.1.0-alpha.7