Honest summarize progress: backend percent pins at 99 the moment tokens start (thinking models stream immediately) then holds there for minutes — so streaming now shows 'Summarizing… working · m:ss' with an indeterminate bar; '0% waiting for model' stays for the pre-token wait; 100 only on success. Transcribe keeps its real percentage.

This commit is contained in:
avi 2026-09-17 14:43:03 -05:00
commit 1eb6d55f4d
3 changed files with 34 additions and 30 deletions

View file

@ -1667,13 +1667,15 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
else -> LiveProgress("transcribing…", null) else -> LiveProgress("transcribing…", null)
} }
s != null && s.status == "running" -> s != null && s.status == "running" ->
if (s.progress != null) if (s.progress == null)
LiveProgress("summarizing… ${s.progress}%", s.progress / 100f) LiveProgress("summarizing… 0% waiting for model" +
// No tokens yet: the LLM is cold or the server is busy waitSuffix(s.startedAt, nowMs), null)
// queueing. Still show a number (0%) — a summarize must // Backend summarize progress saturates at 99 the moment
// always display a percentage (user request) — plus the // the first tokens arrive (thinking models stream
// reason and wait time so 0% doesn't look frozen. // reasoning immediately), so the number means
else LiveProgress("summarizing… 0% waiting for model" + // "streaming", never "how far". Elapsed time is the
// honest signal; the bar stays indeterminate.
else LiveProgress("summarizing… working" +
waitSuffix(s.startedAt, nowMs), null) waitSuffix(s.startedAt, nowMs), null)
else -> null else -> null
} }

View file

@ -777,8 +777,11 @@ fun DetailScreen(state: DesktopState) {
val sJob = detail.jobs.firstOrNull { it.jobType == "summarize" } val sJob = detail.jobs.firstOrNull { it.jobType == "summarize" }
sJob?.takeIf { it.status == "running" || it.status == "queued" }?.let { sJob?.takeIf { it.status == "running" || it.status == "queued" }?.let {
val label = when { val label = when {
// Streaming summarize shows elapsed time, not the backend
// number: it pins at 99% the moment tokens start.
it.status == "running" && it.progress != null -> it.status == "running" && it.progress != null ->
"Summarizing… ${it.progress}%" "Summarizing… working" +
DesktopState.waitSuffix(it.startedAt, System.currentTimeMillis())
it.status == "running" -> "Summarizing… 0% waiting for model" + it.status == "running" -> "Summarizing… 0% waiting for model" +
DesktopState.waitSuffix(it.startedAt, System.currentTimeMillis()) DesktopState.waitSuffix(it.startedAt, System.currentTimeMillis())
else -> "Summary queued" + else -> "Summary queued" +
@ -788,18 +791,11 @@ fun DetailScreen(state: DesktopState) {
color = MaterialTheme.colorScheme.primary) color = MaterialTheme.colorScheme.primary)
if (it.status == "running") { if (it.status == "running") {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
if (it.progress != null) {
LinearProgressIndicator(
progress = { it.progress / 100f },
modifier = Modifier.fillMaxWidth().height(4.dp),
)
} else {
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().height(4.dp), modifier = Modifier.fillMaxWidth().height(4.dp),
) )
} }
} }
}
detail.error?.let { detail.error?.let {
Text(it, color = MaterialTheme.colorScheme.error) Text(it, color = MaterialTheme.colorScheme.error)
if (tJob?.status == "failed") { if (tJob?.status == "failed") {
@ -963,19 +959,18 @@ fun DetailScreen(state: DesktopState) {
val sStatus = sJob?.status val sStatus = sJob?.status
val sProg = sJob?.progress val sProg = sJob?.progress
val sAttempt = sJob?.attempt ?: 0 val sAttempt = sJob?.attempt ?: 0
if (sStatus == "running" && sProg != null && sProg > 0) { // Bar stays indeterminate while running: backend summarize
LinearProgressIndicator( // progress pins at 99 as soon as tokens start, so a
progress = { sProg.coerceIn(0, 99) / 100f }, // determinate bar would freeze near-full for minutes.
modifier = Modifier.fillMaxWidth())
} else {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
Text(buildString { Text(buildString {
append(when { append(when {
sStatus == "queued" -> "Queued…" + sStatus == "queued" -> "Queued…" +
DesktopState.waitSuffix(sJob?.createdAt, DesktopState.waitSuffix(sJob?.createdAt,
System.currentTimeMillis()) System.currentTimeMillis())
sProg != null && sProg > 0 -> "Summarizing… $sProg%" sProg != null && sProg > 0 -> "Summarizing… working" +
DesktopState.waitSuffix(sJob?.startedAt,
System.currentTimeMillis())
else -> "Summarizing… 0% waiting for model" + else -> "Summarizing… 0% waiting for model" +
DesktopState.waitSuffix(sJob?.startedAt, DesktopState.waitSuffix(sJob?.startedAt,
System.currentTimeMillis()) System.currentTimeMillis())

View file

@ -49,13 +49,20 @@ class JobProgressTest {
assertNull(p?.fraction) assertNull(p?.fraction)
} }
@Test fun `summarizing shows backend percentage`() { @Test fun `streaming summarize shows working plus elapsed, not fake percent`() {
val t0 = System.currentTimeMillis() / 1000 * 1000
val iso = java.time.LocalDateTime.ofInstant(
java.time.Instant.ofEpochMilli(t0 - 42_000), java.time.ZoneOffset.UTC)
.withNano(0).toString()
val p = DesktopState.jobProgress( val p = DesktopState.jobProgress(
listOf(job("transcribe", "succeeded"), listOf(job("transcribe", "succeeded"),
job("summarize", "running", progress = 37)), job("summarize", "running", progress = 99, startedIso = iso)),
nowMs = t0,
) )
assertEquals("summarizing… 37%", p?.label) // Backend progress pins at 99 as soon as tokens flow — the label
assertEquals(0.37f, p!!.fraction!!, 0.0001f) // must not present it as a completion percentage.
assertEquals("summarizing… working · 42s", p?.label)
assertNull(p?.fraction)
} }
@Test fun `waiting label carries elapsed time`() { @Test fun `waiting label carries elapsed time`() {