From 0fa558b1a700a2e1bac3510dc3323ed8bdc7a3e5 Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 14 Sep 2026 09:53:40 -0500 Subject: [PATCH] Desktop: adopt server recording by title for pre-mapping files; refresh stale reports missing a summary --- .../kotlin/com/shonar/desktop/DesktopState.kt | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/desktop/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt b/desktop/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt index fa1e93c..e8953de 100644 --- a/desktop/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt +++ b/desktop/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt @@ -718,10 +718,33 @@ class DesktopState(private val appDir: File = defaultAppDir()) { ) _detail.value = d go(Screen.DETAIL) - // If the file is mid-pipeline (pump running, or reprocess started - // elsewhere), show live stage/progress instead of a stale report. - val remoteId = d.remoteId ?: return pollJob = scope.launch { + // Files that predate remote-id mapping have no sidecar: adopt + // the server recording by exact title match so Re-summarize + // appears and finished AI outputs become reachable. + var remoteId = d.remoteId + if (remoteId == null) { + remoteId = adoptServerRecording(d) + if (_detail.value?.file != file) return@launch + } + if (remoteId == null) return@launch + // Report written before a summary existed (or lost a race): + // pull finished outputs so the detail view isn't stale. + val report = d.reportText + if (d.transcript == null && !report.isNullOrBlank() && !report.contains("## Summary")) { + val t = runCatching { parseTranscript(provider.fetchTranscript(remoteId)) } + .getOrNull() + val s = runCatching { parseSummary(provider.fetchSummary(remoteId)) } + .getOrNull() + if (s != null) { + d.transcript = t ?: d.transcript + d.summary = s + saveReport(d) + if (_detail.value?.file == file) _detail.value = d.copy() + } + } + // If the file is mid-pipeline (pump running, or reprocess started + // elsewhere), show live stage/progress instead of a stale report. while (true) { val jobs = runCatching { parseJobs(provider.fetchJobs(remoteId)) } .getOrNull().orEmpty() @@ -736,6 +759,32 @@ class DesktopState(private val appDir: File = defaultAppDir()) { } } + /** Find the server recording matching [d]'s file by title and persist + * the mapping. Returns the recording id, or null when no server-side + * row carries this exact title. */ + private suspend fun adoptServerRecording(d: DetailUi): String? { + val wanted = d.file.nameWithoutExtension + return runCatching { + var cursor: String? = null + var adopted: String? = null + var scanned = 0 + do { + val page = provider.list(cursor) + val matches = page.items.filter { it.title == wanted } + // Only adopt when the title is unambiguous server-side. + if (matches.size == 1) adopted = matches.first().ref.key + if (matches.size > 1) return@runCatching null + cursor = page.nextCursor + scanned += page.items.size + } while (cursor != null && scanned < 400) + adopted?.let { + saveMapping(d.file, RemoteMapping(it, title = wanted)) + d.remoteId = it + } + adopted + }.getOrNull() + } + fun setOverride(model: String?) { _detail.value?.let { it.overrideModel = model; _detail.value = it.copy() } }