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 2b146bf..ed6e0ed 100644 --- a/desktop/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt +++ b/desktop/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt @@ -328,7 +328,12 @@ class DesktopState(private val appDir: File = defaultAppDir()) { } stopEngine() apiProc = spawn(backend, File(venv, "uvicorn").absolutePath, - listOf("shonar.main:app", "--port", "8000"), env) + listOf("shonar.main:app", "--port", "8000") + + // Dev mode: engine hot-reloads on backend source edits. + (if (System.getenv("SHONAR_DEV") == "1") + listOf("--reload", "--reload-dir", "shonar") + else emptyList()), + env) var up = false var waited = 0.0 while (waited < 120.0) { @@ -746,9 +751,23 @@ class DesktopState(private val appDir: File = defaultAppDir()) { // 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() + val fetched = runCatching { parseJobs(provider.fetchJobs(remoteId)) } + if (fetched.isFailure && d.jobs.isEmpty()) { + // Don't fail silently: a dead session used to render as + // an idle screen, indistinguishable from "never started". + d.error = "Couldn't read job status from the engine: " + + (fetched.exceptionOrNull()?.message ?: "unknown error") + } else if (fetched.isSuccess) { + d.error = null + } + val jobs = fetched.getOrNull().orEmpty() if (_detail.value?.file != file) break + // Pick up report rewrites made by the pump/completion paths + // while this screen sits open (they write the file, not us). + if (d.transcript == null) { + val fresh = runCatching { reportFile(file).readText() }.getOrNull() + if (fresh != null && fresh != d.reportText) d.reportText = fresh + } d.jobs = jobs jobLabel(jobs)?.let { d.busy = it } ?: run { d.busy = null } _detail.value = d.copy() diff --git a/desktop/app/src/main/kotlin/com/shonar/desktop/Main.kt b/desktop/app/src/main/kotlin/com/shonar/desktop/Main.kt index f0b7a52..cccc3c6 100644 --- a/desktop/app/src/main/kotlin/com/shonar/desktop/Main.kt +++ b/desktop/app/src/main/kotlin/com/shonar/desktop/Main.kt @@ -37,7 +37,17 @@ fun main() = application { var zoom by remember { mutableStateOf(1f) } Window( onCloseRequest = ::exitApplication, - title = "SHONAR Desktop", + title = "SHONAR Desktop" + run { + // Build stamp so "which window is this?" is answerable at a glance. + val jar = java.io.File( + System.getProperty("java.class.path", "").split(":") + .firstOrNull { it.endsWith(".jar") } ?: "") + if (jar.exists()) { + val fmt = java.time.format.DateTimeFormatter.ofPattern("HH:mm") + .withZone(java.time.ZoneId.systemDefault()) + " · build " + fmt.format(java.time.Instant.ofEpochMilli(jar.lastModified())) + } else "" + }, state = rememberWindowState(width = 1100.dp, height = 800.dp), ) { val baseDensity = LocalDensity.current diff --git a/desktop/desktop-dev.sh b/desktop/desktop-dev.sh new file mode 100755 index 0000000..021eeb4 --- /dev/null +++ b/desktop/desktop-dev.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# SHONAR desktop dev loop: runs the app and auto-restarts it when any +# Kotlin/Gradle source changes (inotifywait). Engine runs in dev mode +# (SHONAR_DEV=1 -> uvicorn --reload), so backend edits apply live too. +# +# Stop with Ctrl-C (or pkill -f desktop-dev.sh). +set -euo pipefail +cd "$(dirname "$0")" + +export SHONAR_DEV=1 +export DISPLAY="${DISPLAY:-:0}" + +WATCH_DIRS=( + "app/src/main/kotlin" + "../../android/app/src/main/java/com/shonar" +) +FLAG="$(pwd)/.dev-restart-flag" + +pkill -f "com.shonar.desktop.MainKt" 2>/dev/null || true +rm -f "$FLAG" +sleep 1 + +( + while inotifywait -qq -e close_write,move \ + --include '\.kt$|\.kts$' "${WATCH_DIRS[@]}" 2>/dev/null; do + echo "[dev] source change -> restarting app…" + touch "$FLAG" + pkill -f "com.shonar.desktop.MainKt" 2>/dev/null || true + done +) & +WATCHER=$! +trap 'kill $WATCHER 2>/dev/null || true; rm -f "$FLAG"' EXIT + +while true; do + rm -f "$FLAG" + ./gradlew run --offline + if [ -f "$FLAG" ]; then + continue # killed by a source edit: rebuild now + fi + rm -f "$FLAG" + # App closed cleanly or build failed — wait for the next edit. + # (Build failure: gradlew prints the error; the loop idles until you fix it.) + inotifywait -qq -e close_write,move --include '\.kt$|\.kts$' "${WATCH_DIRS[@]}" +done