3685ab107dbc94a7841f166930958a8c8ac2d040
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3685ab107d |
Don't report a failure nobody was there to see
Leaving the app for a while and coming back to it greeted you with a five-second read timeout. Work started before the screen went away keeps running -- a build being followed, a download, the manifest poll -- and when the device sleeps or the link drops, that work fails and reported itself. The message said nothing about the server and there was nothing left to do about it, because the thing it described was over before it was read. Every catch now goes through one `failure(e)`, which answers null while the screen is not resumed. Null means *clear*, never *leave*, at every site: a dropped failure that left the card alone would leave a spinner up for an operation that has already stopped. `setProject` joins `setComponent` so both levels are written the same way and a caller can hand the answer over whichever it is. That is only safe because the resume re-read can now run from any state. It was guarded on the list being loaded, which meant a failure ruled out the one thing that would have replaced it -- a card that had gone red stayed red until somebody found the Refresh button. Arriving and resuming are also one event now rather than a LaunchedEffect beside a resume effect, with `loadingList` claimed before the coroutine launches so the two cannot stack two reads in one frame. The other half of the same complaint is the read on the way back in: the link may have been asleep as long as the app was, and the first request across one still coming back times out at the five seconds every request gets. The resume's read alone retries once. It costs nothing when the server really is down, because a refused connection comes back at once rather than waiting out a timeout -- and it is a retry, not a guess. Checked all four ways on the emulator, including the two the change was not written for. Killing the server while the app was backgrounded mid-poll and returning: the list loads, no timeout. Pressing Refresh with the server down: the failure is shown at once, in full. Returning while it is still down: retried, then reported, no stuck spinner. Returning once it is back: the list recovers, which the old guard would have prevented. |
||
|
|
a7f7f4550e |
Output a command produced can be selected and copied
A build's error is the tail of what the compiler actually said, and on a phone there was no way to get it out: the machine that produced it is not the machine in your hand, so a message you cannot copy is one you have to retype into whatever you are fixing it with. Theme.kt's OutputText is the one place that decides this, and every failure message now goes through it -- a component's build or download, a service action, a checkout's remote check, and the log dialog's own failure line. The log body gets a SelectionContainer directly rather than going through OutputText, since it is an AnnotatedString the ANSI renderer coloured inside its own scrolling panel; Copy stays beside it for taking the whole thing. The app's own words are deliberately left alone. Selection handles on a status word like "failed" are noise, and a card that starts a selection on long-press fights the gestures it already has -- so the split is by where the words came from, not by how important they look. Checked on the emulator against a failing build: long-pressing the error on the card raises handles and the Copy toolbar, the state word above it does not, and the log dialog still scrolls in both directions with selection enabled. |
||
|
|
90082bd286 |
Each component builds on its own, and stops reporting when it is done
A project's components were decoupled everywhere except the one place it showed: there was a single build slot per project, and a single card state in the app keyed by project alone. So pressing Update on one client of a two-client project disabled the other client's button for the length of a build it shares nothing with, drew this one's progress bar and download percentage under the other's row, and -- had the button been pressable -- would have been a silent no-op on the server, since a second request while one was running returned without starting anything. The slot is now per component. `Inner` has no `building` flag; a component's own `ComponentRun` with an open `step` is the answer, and `claim` writes that entry synchronously under the lock the route answers from, so nothing can read a just-claimed component as idle -- which the phone would take for a build that had already finished. A failure is recorded against the component whose command it was rather than in the project's one error slot, which two components building at once cannot share. A pull stays exclusive with everything, because there is one checkout and it rewrites the files every component builds from. Releasing it and claiming what it decided to build happen under one lock: a phone polling in the gap would find a project neither pulling nor building and call the run over. The app mirrors the split -- `ProjectState` for the pull and the project-wide Rebuild, `ComponentState` keyed by component for everything one component is asked to do. Two hierarchies rather than one keyed by a pair, so a download has nowhere project-wide to be stored. A component's failure is drawn in its own row beside the Retry that acts on it, which is also where a failed service action now reports. And a finished component shows nothing at all: the elapsed times are gone from both halves of the wire, and its button simply goes back to being pressable. A bar, a count and a last line all describe something happening now, and left up they sit there looking live next to a sibling that genuinely is. Verified on the emulator against test-projects/two-clients, which exists for this: while `tablet` built, its row alone carried the bar and its button alone was disabled, `phone` stayed pressable and silent, and both returned to normal with no timing left behind. |
||
|
|
7eaf79370e |
Build one component instead of every one with a command
Pressing Update on one client of a multi-client project ran every
component's build to get the one that was actually asked for -- cheap
while a project had one APK, expensive the moment it had two and one of
them was slow (an ARM cross-compile, say). /prepare and /build now take
?component= to restrict a run to one named component; absent still means
the whole project, which is what Pull & Build, the project-row Rebuild,
and every single-component project keep doing. There is deliberately no
component-scoped Rebuild -- forcing one component's build without
touching the rest happens by pressing Update on it, which now runs
/prepare scoped to that component.
Verified against a live server driving a scratch two-Apk project:
building one component leaves the other's marker untouched, an unknown
?component= answers 404 naming the project and the component, and naming
none still builds both.
That verification surfaced a second, sharper bug the scoping change had
not caused but did make newly visible: component_is_stale's "never built
at all" check still asked find_apks of the whole project root, left
behind when per-component discovery (
|
||
|
|
c4e19c2e2b |
A project can produce more than one APK
tdep-survey is one checkout with a backend and two independent Android clients, so a project has to be able to declare two Apk components. It could not: APK_PATTERNS was anchored at the project root, which reached the first client and stopped, and package, strip, the variant list and the download all came from AppEntry::apk_component()'s first match -- correct only while a project produced one APK. Each component's builds are now found under its own cwd, which is what cwd already meant everywhere else: the directory the build command runs in, the subtree staleness is scoped to, a server's WorkingDirectory. A component that declares none sits at the project root, so nothing about the single-APK case changes. The alternative -- naming the file on the component -- would have made a component a file path, and an app being a project path rather than a file path is this project's central invariant. Everything derived from an APK follows it onto the component: package, previousPackage, strip, size, mtime, variants and the rename note, in a nested `apk` block that a Server simply doesn't have. Two clients install over different packages, so first-wins would have checked the installed state of one and reported it as the other's -- which looks exactly like a correct answer. For the same reason a download naming no component is refused rather than guessed; naming none still answers for a project with one, which is what lets the frozen /self/apk keep working. On the phone the per-device state is keyed by project and component, so the variant picker moved inside the component's own card, beside the build it picks, and the installed state, size and icon are each their own component's. A project building two clients shows no single icon of its own rather than borrowing the first one's. Measured against the real thing: pointed at tdep-survey, the two clients resolve to their own builds (15 MB and 153 MB), strip applies only to the one that asked for it (153 MB served as 52 MB), pressing Install on one row installed that row's package and left the other row offering Install, and a download with no component named answers 400 saying which flag to pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
db47972a25 |
A component's build progress reads under its buttons, and an unrelated history can be overridden
Two things, both about a card being able to act on what it says. The progress bar, its counts and the last line a component printed now sit below that component's buttons rather than above them. A bar reports on the press that started it, so it reads in the order it happened -- and above, it pushed the buttons down the moment a build began, moving the row somebody had just pressed out from under their finger. A pull that cannot fast-forward because the checkout shares no history with its upstream now offers a way past it. There is no fast-forward between two unrelated histories and there never will be, so the card was one that could never be pulled again, with the only remedy on the build machine -- exactly where the person holding the phone isn't. The card reports the failure as before and a dialog offers a forced pull, naming the branch and the upstream it is about to overwrite; confirming sends ?force=true, which resets onto the upstream instead of merging. Whether it *is* that failure is decided structurally, by `git merge-base` finding no common ancestor, rather than by matching what git printed: those messages are translated, and a button that appeared only on an English build machine would be worse than no button. It travels to the phone as its own field for the same reason. The dirty-tree refusal stays in front of it, so a forced pull can only ever discard something that was committed, and a merely diverged history is not offered it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b0e83059a3 |
dev-updater: build an app on the machine, install it on the phone
A Rust backend that discovers Android projects under configured roots, builds one on request, and serves the APK over pinned TLS on a WireGuard interface; an Android client that lists what is buildable, watches a build, and installs the result. Enrolment carries the token and the CA, so the phone trusts exactly the machine that issued it and nothing else. `AGENTS.md` is the working guide and `README.md` the configuration reference. The shared tunnel-and-TLS code lives in `vendor/wg-app-link`, which ai-app uses too. History before this point was squashed away, and a stale `config.json` went with it: nothing had read that file since the config moved to RON outside the checkout, and what it still held was one machine's absolute paths and the names of projects on it. |