- 06 Sep, 2026 9 commits
-
-
Mahmoud Aglan authored
JUMP TO LATEST. A WhatsApp-style button floating over the transcript — not in the composer, not in the app bar — that appears only once you are meaningfully above the newest message, so it is not permanent clutter. It carries a dot while a run is live, because that is when output is arriving off-screen and the button is worth pressing. USAGE. The refresh button now forces past the server cache, and the card says how old the figures are: "as of 12m ago · updates when a task runs". Claude reports utilisation only inside a run, so a refresh genuinely cannot produce new numbers on its own — showing a spinner that resolves to the same values is what made the button look broken. When there is nothing yet, the server's own explanation is shown instead of a generic line. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
SORTING by last deployed (default), name, status or version. Apps with no deploy timestamp sort last rather than as the epoch: "unknown" is not "ancient". Building sorts above running above stopped, because the one that is changing is the one you opened the tab for. LIVE LOG VIEWER for both build output and runtime logs, polled every 3s. CapRover exposes no log stream, and a poll is what survives a phone moving between networks anyway. Follow-mode sticks to the bottom until you scroll up and stops fighting you — reading history while lines arrive is the normal reason to open a log. Build logs stop polling once the build ends rather than hitting CapRover every three seconds forever. Errors and warnings are coloured, lines are filterable, and the whole buffer copies in one tap. FIXED, and this one was live: every per-app CapRover URL had an escaped dollar sign — '/api/caprover/apps/\$name' — so the path was sent with a literal backslash-dollar-name instead of the app. It came from a heredoc when the client was first written. capApps() has no interpolation, so listing worked and I verified against it; inspect, deploy, set-repo and logs were all broken and none of them had been exercised. Testing the one endpoint that happened to work is not testing the feature. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Three things asked for, plus the plumbing they needed. OPENING A CHAT LANDS AT THE BOTTOM. It used to open at the top of the transcript and leave you scrolling to reach the newest message. It jumps rather than animates, so a long history does not scroll past before settling. MODEL AND EFFORT NOW STICK. The composer's pick was held only in widget state, so it was lost on the next rebuild and every following message silently reverted to Auto. A choice now writes through to the chat itself and survives leaving the screen and restarting the app. AUTO ACTUALLY CHOOSES, AND SAYS WHY. Auto used to mean "send no flag". It now routes per message from the prompt, and every message shows the model, the effort, and the reason — "opus · xhigh · auto: complex task". That visibility is the point: routing nobody can see is routing nobody can correct, and the thresholds are meant to be tuned against real use. What is this repo about -> fable · low (lookup) add a null check to line 42 -> sonnet · medium (code change) Refactor billing, migrate the schema -> opus · xhigh (complex task) Bias is toward the cheaper option: under-serving a hard prompt costs one visible retry, over-serving every trivial one quietly burns the subscription all day. CAPROVER TAB. See what is deployed, what it deploys from, redeploy, repoint an app at a different repo/branch, and read the build log — without leaving the phone. Deliberately scoped: - The phone never holds the CapRover password. It authenticates to DevPilot, which holds that credential server-side, so a lost device is a token change here rather than a password rotation across everything. - Env var VALUES never leave the server — the app shows which keys exist, not what they hold. Those are database passwords and API keys for 54 apps. - Creating and deleting apps are not exposed at all. Irreversible actions against live client deployments belong at a desk, not one mis-tap away. - Redeploy is confirmed, because it restarts a live service and you are by definition not at a desk when you press it. - Changing the deploy source reads the whole app definition and sends it back with only the repo altered: CapRover's update endpoint replaces the object, so a partial body silently drops volumes and env vars. Verified against the real CapRover: 54 apps enumerated with versions, images and git sources. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Both project pickers now render the server's CLAUDE.md-derived description under the repo name. Picking `el-captain` when you meant `el-captain-sports-management` is easy when the list is names alone, and the consequence — an agent answering confidently about the wrong codebase — looks like a bug in the app rather than a mis-tap. Degrades cleanly: an older server sends no description, the field parses to an empty string, and the tile falls back to the name on its own. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The chat showed a spinner for the whole run and only rendered when it finished, even though the server's stream was correct — verified frame by frame against production, one per second, each tool call and result as it happened: 10:01:35
⚙ $ echo STEP1 10:01:35 → STEP1 10:01:36⚙ $ echo STEP2 So the transport was fine and the client was not receiving it. Rather than chase a long-lived stream through a proxy and a mobile network — where buffering, an idle timeout or a handover all produce exactly this silence — the feed no longer depends on the stream at all. Two sources now race: SSE (instant) and a 1.5s poll of the running task (guaranteed). Whichever is further ahead wins, compared by length because both render the same append-only text. If the stream never delivers a byte the feed still updates every 1.5s; if it works, it wins every time and the poll is invisible. Polling targets GET /api/tasks/:id rather than the whole chat, so it carries one run's output instead of every message's body on every tick. Also: the stream now replays with from=0, so opening a chat that is already mid-run shows what has happened so far instead of only what arrives next. And "Working…" no longer appears twice — the bubble header says it, so the body no longer repeats it. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Two problems from the first real use of the app. RENDERING. Every line of a run was drawn as flat monospace text, so the parts that were markdown arrived as their source: tables as rows of pipe characters, `**bold**` as asterisks, headings as hashes. That is the "gibberish between real replies" — there was never any gibberish, only unrendered markdown sitting between the parts that happened to contain none. The server already emits a structured line vocabulary (
▶ header,⚙ tool, → result, · thinking, ■ footer, everything else prose). AgentOutput now groups those lines into blocks and renders each as what it is: prose -> MarkdownBody, with tables, code blocks, lists and headings tool -> a compact one-line step, its result collapsed behind it thinking-> a single pulsing marker; consecutive ones collapse header -> present but recessive; useful only when something goes wrong A tool result is attached to the step above it rather than printed inline, so a 500-character command output no longer sits between two paragraphs. Tap to expand it, long-press the step to copy the command. THE ICON. I rebuilt the flame by hand instead of taking it from the APK that was sitting in front of me the whole time — a worse copy of an asset I already had. The original icon.png, icon_foreground.png and flame.png are lifted from the old build's flutter_assets and are now both the launcher icon and the in-app mark, loaded from the same file so the two cannot drift. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Two follow-ons from pinning AGP, both caught by build validation rather than at runtime, which is the argument against reaching for --android-skip-build-dependency-validation to make a build "pass". Kotlin: I had dropped it to 2.1.0 alongside the AGP pin, assuming AGP 8.x required it. It does not — KGP and AGP version independently, and Flutter 3.47 requires Kotlin >= 2.2.20, so my downgrade was the whole problem. Back to 2.4.0. There was no circular constraint here; there was an unnecessary edit. re_editor: 0.7.0 fails to compile against Flutter 3.47 — its _CodeInputController implements DeltaTextInputClient without onFocusReceived(), which the framework has since added. The caret constraint ^0.7.0 pinned it below 0.8, hiding the four newer releases; 0.10.0 implements it. Analysis stays clean across the upgrade, so no call sites changed. Final working combination: AGP 8.11.1 · Gradle 8.14 · Kotlin 2.4.0 · re_editor 0.10.0 Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The release build failed while evaluating flutter_inappwebview_android: getDefaultProguardFile('proguard-android.txt') is no longer supported AGP 9 removed that API. The plugin still calls it at configuration time, in both its debug and release buildTypes, so the failure happens whether or not minification is on. 1.1.3 is the newest release of that plugin — 6.1.5 is the current top-level package — so there is nothing to upgrade to; the template simply pinned an AGP ahead of the plugin ecosystem. com.android.application 9.1.0 -> 8.7.3 kotlin.android 2.4.0 -> 2.1.0 (matched to AGP 8.x) gradle 9.3.1 -> 8.12 (AGP 8.7 does not support Gradle 9) Fixed here rather than by patching ~/.pub-cache: that cache is generated, shared between projects, outside git, and erased by `pub cache repair`. A fix there works on exactly one machine and silently disappears — including on the server, once Flutter moves into the Docker image. Reverse this pin when the plugin ships an AGP 9 build. The launcher icon is generated by a Pillow script from the same curve geometry as the in-app FlameMark painter, so the icon and the mark cannot drift apart. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Ground-up replacement for the DevPilot APK whose source was lost in the Sep 1 wipe. That app could only be maintained by guessing at an opaque binary: two days went into inferring its wire contract from `strings` on libapp.so, and the last bug (every text file rendering as "Binary file") was never solvable from the server side at all. Owning the source ends that entire class of problem. Five tabs, each backed by a real endpoint: VS Code code-server in a webview, host derived from the API host Chats multi-turn conversations, live SSE, per-message model and effort, attachments from the phone, local notification when a run lands Files project-scoped browser, re_editor with syntax highlighting, markdown rendered with mermaid diagrams, create/rename/delete/upload Git per-repo status, coloured diff, pull/commit/push Settings Claude usage against both rolling windows, connection, sign out Decisions worth recording: - models.dart parses defensively on purpose. Every fromJson is total and a wrong-typed field degrades to a default. The old client did the opposite and one bad field made *every* file unopenable, because a throw inside fromJson loses the whole object rather than one value. - Binary is believed only when the server explicitly says so. Treating "unknown" as binary is precisely the bug that made the previous app useless. - SSE is read off a raw HttpClient, not Dio: it is a line protocol held open indefinitely and Dio is built around a body that ends. The stream resumes with ?from=N so backgrounding the phone does not lose output. - Mermaid renders in a webview because no Dart package draws it; the source is passed as a JSON literal so quotes and newlines cannot break out, and it falls back to showing the source offline instead of an empty box. - Tabs live in an IndexedStack so scroll position, open directory and in-flight streams survive switching away — the app exists to leave work running. - No FCM. The server has nothing to push with, and the only event worth an interruption is one the app already observes. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-