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() } }