OXYGENHUB

The phone runner says what it runs, on its card and notification (refs #8); pins OxygenFlow main 0b152017dee3

Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 86de9233aaf1
When
· 2026-10-29 01:14 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
0b152017dee3d4d6b9fb9a0aa6e0932a49f5006376dfc745e320ec65bed483e6
Depends on
26530d9758ee 41e2e862abce 4aee5293461d 57c0824300db 8440d51a2275 aa21eb5e0e5b c53e328b2db4
Depended on by
aa627197ec18 6823c243eaac
In bookmarks
main
In tags
v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9
Edits
17 atoms, 0 tree operations, 8 files

Browse the tree at this change

modified CHANGELOG.md +1 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
- **The phone runner says what it runs (#8):** while it serves, the runner card lists the jobs it is running (owner/name #run job), and its notification says "Running owner/name #7 test" instead of waiting, from OxygenFlow's `runner::running()` (pins OxygenFlow main).
5 6
- **The phone runner rests when warm (#8):** besides charging and Wi-Fi, the runner now needs the phone cool: when the system says it is throttling (moderate or worse) or the battery is above 40 °C it takes nothing more, finishes a job due within a minute and hands back any other, as off charge, and serves again once it has cooled. The card and the notification say why it waits.
6 7
- **A proposal's description is Markdown:** it showed as plain text, `##`, `**`, backticks and table pipes included, with the source's line breaks; it now renders as an issue's does (headings, emphasis, code, lists and tables, lines joined into paragraphs).
7 8
…
31 unchanged lines
modified app/src/main/java/io/oxygenhub/android/runner/PhoneRunner.kt +15 −0
…
51 unchanged lines
52 52
    private val _state = MutableStateFlow<State>(State.Unregistered)
53 53
    val state: StateFlow<State> = _state.asStateFlow()
54 54
55 +
    /** The jobs it is running now, "owner/name #run job" (#8), while it serves. */
56 +
    private val _jobs = MutableStateFlow<List<String>>(emptyList())
57 +
    val jobs: StateFlow<List<String>> = _jobs.asStateFlow()
58 +
59 +
    internal fun setJobs(jobs: List<String>) {
60 +
        _jobs.value = jobs
61 +
    }
62 +
63 +
    /** The notification's line for [jobs]: what it runs, or that it waits for work. */
64 +
    fun jobsLine(jobs: List<String>): String = when (jobs.size) {
65 +
        0 -> "Waiting for WASM jobs, on charge and Wi-Fi"
66 +
        1 -> "Running ${jobs[0]}"
67 +
        else -> "Running ${jobs.size} jobs: ${jobs.joinToString(", ")}"
68 +
    }
69 +
55 70
    internal fun set(state: State) {
56 71
        _state.value = state
57 72
    }
…
112 unchanged lines
modified app/src/main/java/io/oxygenhub/android/runner/RunnerCard.kt +3 −0
…
41 unchanged lines
42 42
fun RunnerCard(api: HubApi, hub: String, canWrite: Boolean) {
43 43
    val context = LocalContext.current
44 44
    val state by PhoneRunner.state.collectAsStateWithLifecycle()
45 +
    val jobs by PhoneRunner.jobs.collectAsStateWithLifecycle()
45 46
    var busy by remember { mutableStateOf(false) }
46 47
    var error by remember { mutableStateOf<String?>(null) }
47 48
    val scope = rememberCoroutineScope()
…
23 unchanged lines
71 72
                is PhoneRunner.State.Failed -> "Stopped: ${s.message}" to status.bad
72 73
            }
73 74
            Text(line, style = MaterialTheme.typography.bodyMedium, color = colour)
75 +
            // What it runs now (#8).
76 +
            jobs.forEach { Text("▸ $it", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) }
74 77
            error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = status.bad) }
75 78
            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
76 79
                when (state) {
…
30 unchanged lines
modified app/src/main/java/io/oxygenhub/android/runner/RunnerService.kt +24 −1
…
18 unchanged lines
19 19
import android.os.PowerManager
20 20
import io.oxygenhub.android.MainActivity
21 21
import io.oxygenhub.android.R
22 +
import uniffi.rust_core.runnerRunning
22 23
import uniffi.rust_core.runnerServe
23 24
import uniffi.rust_core.runnerStop
24 25
…
88 unchanged lines
113 114
        }
114 115
        val home = PhoneRunner.home(this).path
115 116
        PhoneRunner.set(PhoneRunner.State.Running)
116 -
        notify("Waiting for WASM jobs, on charge and Wi-Fi")
117 +
        notify(PhoneRunner.jobsLine(emptyList()))
118 +
        watchJobs()
117 119
        serving = Thread {
118 120
            val failed = try {
119 121
                runnerServe(home)
…
8 unchanged lines
128 130
        }
129 131
    }
130 132
133 +
    /** While serving, what it runs (#8): on the card, and in the notification when it changes. */
134 +
    private fun watchJobs() {
135 +
        Thread {
136 +
            var shown: List<String>? = null
137 +
            while (serving != null) {
138 +
                val now = runnerRunning()
139 +
                if (now != shown) {
140 +
                    PhoneRunner.setJobs(now)
141 +
                    if (PhoneRunner.state.value == PhoneRunner.State.Running) notify(PhoneRunner.jobsLine(now))
142 +
                    shown = now
143 +
                }
144 +
                Thread.sleep(3_000)
145 +
            }
146 +
            PhoneRunner.setJobs(emptyList())
147 +
        }.apply {
148 +
            name = "oxygenflow-runner-jobs"
149 +
            isDaemon = true
150 +
            start()
151 +
        }
152 +
    }
153 +
131 154
    /** Serving ended: wait for the charger and Wi-Fi again while the owner wants it, else stop. */
132 155
    @Synchronized
133 156
    private fun served(failed: String?) {
…
73 unchanged lines
modified app/src/test/java/io/oxygenhub/android/runner/PhoneRunnerTest.kt +7 −0
…
16 unchanged lines
17 17
            PhoneRunner.tooWarm(PowerManager.THERMAL_STATUS_NONE, 415),
18 18
        )
19 19
    }
20 +
21 +
    @Test
22 +
    fun whatItRunsReadsPlainly() {
23 +
        assertEquals("Waiting for WASM jobs, on charge and Wi-Fi", PhoneRunner.jobsLine(emptyList()))
24 +
        assertEquals("Running andy/demo #7 test", PhoneRunner.jobsLine(listOf("andy/demo #7 test")))
25 +
        assertEquals("Running 2 jobs: a/b #1 x, a/b #1 y", PhoneRunner.jobsLine(listOf("a/b #1 x", "a/b #1 y")))
26 +
    }
20 27
}
replaced oxygenflow +0 −0

No line changes.

modified rust-core/Cargo.lock +4 −4
…
895 unchanged lines
896 896
897 897
[[package]]
898 898
name = "flow"
899 -
version = "0.5.0"
899 +
version = "0.5.2"
900 900
dependencies = [
901 901
 "anyhow",
902 902
 "blake3",
…
18 unchanged lines
921 921
922 922
[[package]]
923 923
name = "flow-core"
924 -
version = "0.5.0"
924 +
version = "0.5.2"
925 925
dependencies = [
926 926
 "blake3",
927 927
 "curve25519-dalek",
…
10 unchanged lines
938 938
939 939
[[package]]
940 940
name = "flow-init"
941 -
version = "0.5.0"
941 +
version = "0.5.2"
942 942
dependencies = [
943 943
 "libc",
944 944
 "serde",
…
408 unchanged lines
1353 1353
1354 1354
[[package]]
1355 1355
name = "jet"
1356 -
version = "0.5.0"
1356 +
version = "0.5.2"
1357 1357
dependencies = [
1358 1358
 "blake3",
1359 1359
 "hex",
…
2107 unchanged lines
modified rust-core/src/lib.rs +6 −0
…
76 unchanged lines
77 77
}
78 78
79 79
/// The tiers this phone offers (the WASM tier), and why not the others.
80 +
/// What the runner is running now: "owner/name #run job" for each job (#8).
80 81
#[uniffi::export]
82 +
pub fn runner_running() -> Vec<String> {
83 +
    flow::runner::running()
84 +
}
85 +
86 +
#[uniffi::export]
81 87
pub fn runner_tiers(home: String) -> Vec<String> {
82 88
    let Ok(h) = flow::home::Home::locate(Some(Path::new(&home))) else { return vec![] };
83 89
    match flow::runner::tiers(&h) {
…
26 unchanged lines