From a70247aa255f214b8c3b0085b4412e39e81160f9 Mon Sep 17 00:00:00 2001 From: Ross Tomsic Date: Wed, 23 Sep 2026 20:20:54 -0400 Subject: [PATCH 1/2] Show isolated label on saved transcript cards Room already kept the execution mode, but the history dialog only printed status and exit code, so an isolated failure read like a persistent one. --- .../threadline/ConnectionFormRetentionTest.kt | 19 +++++++++++++++++++ .../java/dev/threadline/TranscriptHistory.kt | 10 ++++++++-- docs/BACKLOG.md | 8 ++++---- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt b/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt index 822713a..abfa6f5 100644 --- a/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt +++ b/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.Density import dev.threadline.core.model.HostEndpoint import dev.threadline.core.model.HostProfile import dev.threadline.core.model.SessionCredential +import dev.threadline.core.shell.CommandExecutionMode import dev.threadline.core.shell.CommandId import dev.threadline.core.transcript.CommandOutput import dev.threadline.core.transcript.CommandStatus @@ -583,6 +584,8 @@ class ConnectionFormRetentionTest { compose.onNodeWithText("printf saved").assertExists() compose.onNodeWithTag(TranscriptHistoryTags.OUTPUT_PREFIX + "command-session-1") .assertExists() + compose.onNodeWithText("Succeeded · exit 0").assertExists() + compose.onNodeWithText("Isolated · Failed · exit 1").assertExists() compose.onNodeWithText("Back").performClick() compose.onNodeWithTag(TranscriptHistoryTags.DELETE_PREFIX + "session-1") @@ -1203,6 +1206,22 @@ private fun transcriptSession(id: String) = SavedTranscriptSession( ), commandTruncated = false, ), + SavedTranscriptTurn( + turn = CommandTurn( + id = CommandId("isolated-$id"), + command = "set -e; false", + executionMode = CommandExecutionMode.ISOLATED, + directoryAtStart = "/tmp", + submittedAtMillis = 3, + startedAtMillis = 3, + completedAtMillis = 4, + status = CommandStatus.FAILED, + exitStatus = 1, + currentDirectory = "/tmp", + output = CommandOutput(), + ), + commandTruncated = false, + ), ), ) diff --git a/app/src/main/java/dev/threadline/TranscriptHistory.kt b/app/src/main/java/dev/threadline/TranscriptHistory.kt index 4fbc418..57951b8 100644 --- a/app/src/main/java/dev/threadline/TranscriptHistory.kt +++ b/app/src/main/java/dev/threadline/TranscriptHistory.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp +import dev.threadline.core.shell.CommandExecutionMode import dev.threadline.core.transcript.CommandStatus import dev.threadline.data.transcript.SavedTranscriptSession import dev.threadline.data.transcript.SavedTranscriptSessionSummary @@ -257,7 +258,7 @@ internal fun TranscriptHistorySection( ) } Text( - savedTurnStatus(turn.status, turn.exitStatus), + savedTurnStatus(turn.executionMode, turn.status, turn.exitStatus), style = MaterialTheme.typography.bodySmall, ) if (turn.output.plainText.isNotEmpty()) { @@ -403,7 +404,12 @@ internal fun TranscriptHistorySection( } } -private fun savedTurnStatus(status: CommandStatus, exitStatus: Int?): String = buildString { +private fun savedTurnStatus( + executionMode: CommandExecutionMode, + status: CommandStatus, + exitStatus: Int?, +): String = buildString { + if (executionMode == CommandExecutionMode.ISOLATED) append("Isolated · ") append(status.name.lowercase().replaceFirstChar(Char::uppercase)) exitStatus?.let { append(" · exit ") diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 3c08ad8..3a5ae38 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -232,10 +232,10 @@ may improve session summaries, command-card hierarchy, navigation, search, and the distinction between live styled output and persisted plain text. Alpha.8 exposed one concrete omission: Room retains whether a command ran -isolated, but the saved-transcript dialog formats only status and exit code. An -isolated failed turn therefore appears as `Failed · exit 1` without its -execution contract. Add the execution-mode label to saved cards when history -presentation is revised, and cover it with a UI test. +isolated, but the saved-transcript dialog formatted only status and exit code, +so an isolated failed turn appeared as `Failed · exit 1` without its execution +contract. Saved cards now lead with `Isolated ·` like live cards do, covered by +the saved-transcript Compose test. Preserve bounded retention, explicit deletion, no-write ephemeral sessions, and the rule that history must not accidentally become credential storage. From 15bdabcd440786675e696a01aa0e6a886c7b0f2e Mon Sep 17 00:00:00 2001 From: Ross Tomsic Date: Wed, 23 Sep 2026 20:25:04 -0400 Subject: [PATCH 2/2] Use live status labels on saved transcript cards Saved cards derived the label from the enum name, so UNKNOWN showed as "Unknown" instead of "Status unknown". --- .../threadline/ConnectionFormRetentionTest.kt | 16 ++++++++++++++++ .../java/dev/threadline/TranscriptHistory.kt | 2 +- .../main/java/dev/threadline/TranscriptScreen.kt | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt b/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt index abfa6f5..c1f26d9 100644 --- a/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt +++ b/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt @@ -586,6 +586,7 @@ class ConnectionFormRetentionTest { .assertExists() compose.onNodeWithText("Succeeded · exit 0").assertExists() compose.onNodeWithText("Isolated · Failed · exit 1").assertExists() + compose.onNodeWithText("Status unknown").assertExists() compose.onNodeWithText("Back").performClick() compose.onNodeWithTag(TranscriptHistoryTags.DELETE_PREFIX + "session-1") @@ -1222,6 +1223,21 @@ private fun transcriptSession(id: String) = SavedTranscriptSession( ), commandTruncated = false, ), + SavedTranscriptTurn( + turn = CommandTurn( + id = CommandId("unknown-$id"), + command = "sleep 60", + directoryAtStart = "/tmp", + submittedAtMillis = 5, + startedAtMillis = 5, + completedAtMillis = null, + status = CommandStatus.UNKNOWN, + exitStatus = null, + currentDirectory = "/tmp", + output = CommandOutput(), + ), + commandTruncated = false, + ), ), ) diff --git a/app/src/main/java/dev/threadline/TranscriptHistory.kt b/app/src/main/java/dev/threadline/TranscriptHistory.kt index 57951b8..2f55286 100644 --- a/app/src/main/java/dev/threadline/TranscriptHistory.kt +++ b/app/src/main/java/dev/threadline/TranscriptHistory.kt @@ -410,7 +410,7 @@ private fun savedTurnStatus( exitStatus: Int?, ): String = buildString { if (executionMode == CommandExecutionMode.ISOLATED) append("Isolated · ") - append(status.name.lowercase().replaceFirstChar(Char::uppercase)) + append(status.label) exitStatus?.let { append(" · exit ") append(it) diff --git a/app/src/main/java/dev/threadline/TranscriptScreen.kt b/app/src/main/java/dev/threadline/TranscriptScreen.kt index b92b9b7..64cf33f 100644 --- a/app/src/main/java/dev/threadline/TranscriptScreen.kt +++ b/app/src/main/java/dev/threadline/TranscriptScreen.kt @@ -1146,7 +1146,7 @@ private fun formatDuration(milliseconds: Long): String = String.format(java.util.Locale.ROOT, "%.1f s", milliseconds / 1_000.0) } -private val CommandStatus.label: String +internal val CommandStatus.label: String get() = when (this) { CommandStatus.SUBMITTED -> "Submitted" CommandStatus.RUNNING -> "Running"