From 1eb6d55f4d19bef1af9410abef04047c5c911302 Mon Sep 17 00:00:00 2001 From: avi Date: Thu, 17 Sep 2026 14:43:03 -0500 Subject: [PATCH] =?UTF-8?q?Honest=20summarize=20progress:=20backend=20perc?= =?UTF-8?q?ent=20pins=20at=2099=20the=20moment=20tokens=20start=20(thinkin?= =?UTF-8?q?g=20models=20stream=20immediately)=20then=20holds=20there=20for?= =?UTF-8?q?=20minutes=20=E2=80=94=20so=20streaming=20now=20shows=20'Summar?= =?UTF-8?q?izing=E2=80=A6=20working=20=C2=B7=20m:ss'=20with=20an=20indeter?= =?UTF-8?q?minate=20bar;=20'0%=20waiting=20for=20model'=20stays=20for=20th?= =?UTF-8?q?e=20pre-token=20wait;=20100=20only=20on=20success.=20Transcribe?= =?UTF-8?q?=20keeps=20its=20real=20percentage.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/shonar/desktop/DesktopState.kt | 16 +++++---- .../main/kotlin/com/shonar/desktop/Screens.kt | 33 ++++++++----------- .../com/shonar/desktop/JobProgressTest.kt | 15 ++++++--- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt index 9700edb..5419b8f 100644 --- a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt +++ b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt @@ -1667,13 +1667,15 @@ class DesktopState(private val appDir: File = defaultAppDir()) { else -> LiveProgress("transcribing…", null) } s != null && s.status == "running" -> - if (s.progress != null) - LiveProgress("summarizing… ${s.progress}%", s.progress / 100f) - // No tokens yet: the LLM is cold or the server is busy - // queueing. Still show a number (0%) — a summarize must - // always display a percentage (user request) — plus the - // reason and wait time so 0% doesn't look frozen. - else LiveProgress("summarizing… 0% waiting for model" + + if (s.progress == null) + LiveProgress("summarizing… 0% waiting for model" + + waitSuffix(s.startedAt, nowMs), null) + // Backend summarize progress saturates at 99 the moment + // the first tokens arrive (thinking models stream + // reasoning immediately), so the number means + // "streaming", never "how far". Elapsed time is the + // honest signal; the bar stays indeterminate. + else LiveProgress("summarizing… working" + waitSuffix(s.startedAt, nowMs), null) else -> null } diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index 915f661..d0fe6bf 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -777,8 +777,11 @@ fun DetailScreen(state: DesktopState) { val sJob = detail.jobs.firstOrNull { it.jobType == "summarize" } sJob?.takeIf { it.status == "running" || it.status == "queued" }?.let { 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 -> - "Summarizing… ${it.progress}%" + "Summarizing… working" + + DesktopState.waitSuffix(it.startedAt, System.currentTimeMillis()) it.status == "running" -> "Summarizing… 0% waiting for model" + DesktopState.waitSuffix(it.startedAt, System.currentTimeMillis()) else -> "Summary queued" + @@ -788,16 +791,9 @@ fun DetailScreen(state: DesktopState) { color = MaterialTheme.colorScheme.primary) if (it.status == "running") { Spacer(Modifier.height(4.dp)) - if (it.progress != null) { - LinearProgressIndicator( - progress = { it.progress / 100f }, - modifier = Modifier.fillMaxWidth().height(4.dp), - ) - } else { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth().height(4.dp), - ) - } + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().height(4.dp), + ) } } detail.error?.let { @@ -963,19 +959,18 @@ fun DetailScreen(state: DesktopState) { val sStatus = sJob?.status val sProg = sJob?.progress val sAttempt = sJob?.attempt ?: 0 - if (sStatus == "running" && sProg != null && sProg > 0) { - LinearProgressIndicator( - progress = { sProg.coerceIn(0, 99) / 100f }, - modifier = Modifier.fillMaxWidth()) - } else { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } + // Bar stays indeterminate while running: backend summarize + // progress pins at 99 as soon as tokens start, so a + // determinate bar would freeze near-full for minutes. + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) Text(buildString { append(when { sStatus == "queued" -> "Queued…" + DesktopState.waitSuffix(sJob?.createdAt, 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" + DesktopState.waitSuffix(sJob?.startedAt, System.currentTimeMillis()) diff --git a/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt b/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt index 90ad439..c78754f 100644 --- a/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt +++ b/app/src/test/kotlin/com/shonar/desktop/JobProgressTest.kt @@ -49,13 +49,20 @@ class JobProgressTest { 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( listOf(job("transcribe", "succeeded"), - job("summarize", "running", progress = 37)), + job("summarize", "running", progress = 99, startedIso = iso)), + nowMs = t0, ) - assertEquals("summarizing… 37%", p?.label) - assertEquals(0.37f, p!!.fraction!!, 0.0001f) + // Backend progress pins at 99 as soon as tokens flow — the label + // must not present it as a completion percentage. + assertEquals("summarizing… working · 42s", p?.label) + assertNull(p?.fraction) } @Test fun `waiting label carries elapsed time`() {