diff --git a/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt b/app/src/androidTest/java/dev/threadline/ConnectionFormRetentionTest.kt index 822713a..c1f26d9 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,9 @@ 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("Status unknown").assertExists() compose.onNodeWithText("Back").performClick() compose.onNodeWithTag(TranscriptHistoryTags.DELETE_PREFIX + "session-1") @@ -1203,6 +1207,37 @@ 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, + ), + 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 4fbc418..2f55286 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,8 +404,13 @@ internal fun TranscriptHistorySection( } } -private fun savedTurnStatus(status: CommandStatus, exitStatus: Int?): String = buildString { - append(status.name.lowercase().replaceFirstChar(Char::uppercase)) +private fun savedTurnStatus( + executionMode: CommandExecutionMode, + status: CommandStatus, + exitStatus: Int?, +): String = buildString { + if (executionMode == CommandExecutionMode.ISOLATED) append("Isolated · ") + 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" 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.