Voice chips follow the saved report's voice in saved-report view

Opening a recording whose live summary row isn't loaded fell through
to sumTone (null = Neutral), so the pill showed Neutral while the
report text on screen read "*Voice: funny*". chipTone now reads the
report's own *Voice: X* metadata line as the fallback, so the chips
can never contradict the artifact being displayed. Running job and
live summary rows still take precedence. 3 new tests (reportVoice).
This commit is contained in:
avi 2026-09-18 19:58:42 -05:00
commit 70483b4cf8
3 changed files with 38 additions and 0 deletions

View file

@ -1957,6 +1957,21 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
listOf(uploadStage, tStage, sStage, fStage), elapsed, eta) listOf(uploadStage, tStage, sStage, fStage), elapsed, eta)
} }
/** Voice recorded inside a saved .transcript.md report — the
* `*Voice: X*` metadata line written by Reports.renderReport.
* Used when the live summary row isn't loaded (saved-report
* view) so the voice chips can't contradict the report on
* screen. "neutral" (or absent) maps to null. Pure — tested. */
fun reportVoice(reportText: String?): String? {
reportText ?: return null
val line = reportText.lineSequence().firstOrNull {
it.trim().startsWith("*Voice:")
} ?: return null
val v = line.trim().removePrefix("*Voice:").removeSuffix("*")
.trim()
return v.takeIf { it.isNotBlank() && !it.equals("neutral", true) }
}
const val KEY_URL = "server.url" const val KEY_URL = "server.url"
const val KEY_FOLDER = "library.folder" const val KEY_FOLDER = "library.folder"
const val KEY_PASSWORD = "local.password" const val KEY_PASSWORD = "local.password"

View file

@ -683,6 +683,11 @@ fun DetailScreen(state: DesktopState) {
val chipTone: String? = when { val chipTone: String? = when {
sJobNow != null && sJobNow.status in setOf("queued", "running") -> sJobNow.tone sJobNow != null && sJobNow.status in setOf("queued", "running") -> sJobNow.tone
sSummary != null -> sSummary.tone sSummary != null -> sSummary.tone
// Live summary row unavailable (saved-report view): read the
// voice out of the report itself so the chips can't contradict
// the "*Voice: funny*" text on screen (user: report said funny,
// Neutral pill was highlighted).
detail.reportText != null -> DesktopState.reportVoice(detail.reportText)
else -> sumTone else -> sumTone
} }

View file

@ -189,4 +189,22 @@ class JobProgressTest {
assertNull(DesktopState.etaFrom(300, 99)) // no promising the last token assertNull(DesktopState.etaFrom(300, 99)) // no promising the last token
assertEquals(25L, DesktopState.etaFrom(100, 80)) assertEquals(25L, DesktopState.etaFrom(100, 80))
} }
// ---- reportVoice: chips follow the saved report's own voice ----
@Test fun `reportVoice reads the metadata line`() {
val md = "# X\n## Summary\n*Voice: funny*\n\ntext"
assertEquals("funny", DesktopState.reportVoice(md))
}
@Test fun `reportVoice neutral and absent map to null`() {
assertNull(DesktopState.reportVoice("# X\n*Voice: neutral*\n"))
assertNull(DesktopState.reportVoice("# X\nno voice line\n"))
assertNull(DesktopState.reportVoice(null))
}
@Test fun `reportVoice keeps multi-word tones intact`() {
assertEquals("dry, witty",
DesktopState.reportVoice("*Voice: dry, witty*"))
}
} }