1. 18 Sep, 2026 1 commit
    • Mahmoud Aglan's avatar
      feat: parallel runs made visible, real deploys, and twenty daily-use features · 84fd69a4
      Mahmoud Aglan authored
      The two that hurt
      - Two chats on one repo never ran together, and the app showed the waiting
        one as "working". The server now allows several runs per repo (Settings
        sets how many), and a queued run says what it is waiting for, everywhere
        it appears.
      - Deploy did nothing. CapRover rejects the call the app was making, so the
        button reported success and shipped nothing; the fix builds the head of
        the app's branch. Apps with no repository say so instead of pretending.
      
      Runs
       1. Several runs at once, per repo and overall, both adjustable in Settings.
       2. A "running now" sheet: everything working or waiting, with why, and a
          stop button for each. The Chats tab carries a live count.
       3. Type while a run is going and the message queues — it sends itself the
          moment that run ends.
       4. Fork a chat: same repo, last prompt ready to edit, both run in parallel.
      
      Chats
       5. Dictation, with a language picker on long-press (an Arabic sentence in
          an English recogniser is nonsense).
       6. A file picker that inserts a repo path into the prompt.
       7. Search inside conversations — titles, prompts and answers — not just
          titles, with the matching line shown.
       8. Each finished run shows how long it took, and its exit code if it failed.
       9. Pin a chat (long-press) to keep it at the top.
      10. Tapping a finished-run notification opens that chat, even from cold.
      
      Git
      11. A commit message written from the actual diff, then edited before use.
      12. The branch chip switches branch, and creates one.
      13. Commit history, each commit openable as its own patch.
      
      Files
      14. Content search across the repo, opening the file at the matching line.
      15. Starred files and recents per project.
      16. "Diff vs HEAD" on the open file.
      
      Deploy
      17. Deploy = build the latest commit on the branch (the fix above).
      18. Roll back to a version CapRover still has the image for.
      19. Restart, and scale instances (zero is a deliberate stop).
      20. Box health in Settings: CPU, memory, disk, and what is running.
      
      Version 1.5.0+15.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      84fd69a4
  2. 13 Sep, 2026 1 commit
    • Mahmoud Aglan's avatar
      feat: download, zip/unzip, PDF export, and ten quality-of-life fixes · 90a71482
      Mahmoud Aglan authored
      Files
      - Download to the phone (Android's save dialog) or Share — a file, a folder
        as .zip, or a multi-selection. Streamed to disk with progress and Cancel.
      - Long-press selects; a selection zips, downloads, shares or deletes. A ⋮
        on every row carries the per-file actions.
      - Compress to .zip and Extract, always into a new folder (the server never
        overwrites). Tapping a .zip lists its contents first.
      - Export as PDF, made on the phone: markdown (tables, Arabic RTL, repo
        images inlined, mermaid rendered to SVG in a headless WebView because the
        print WebView runs without JavaScript), code as a numbered highlighted
        listing, images, a folder's markdown as one document with contents, and a
        whole chat. Saved to the phone, shared, or put into the repo beside the
        source under a name that never overwrites.
      
      Quality of life
       1. Retry / Edit & resend under a failed run.
       2. Per-chat drafts that survive leaving the chat.
       3. Quick-prompt chips; save your own, long-press to remove.
       4. Screen stays on while a watched run is live (toggle in Settings).
       5. Image preview with pinch and double-tap zoom.
       6. Copy path, and "Ask Claude about this": a new chat, path pre-typed.
       7. Files reopens the folder last used in each project.
       8. Sort by name, date or size; size and age on every row.
       9. Git lists changed files per repo; each opens its own diff with a +/−
          count and Discard for unstaged changes; untracked files show as new.
      10. Text size in Settings, app-wide and in the code editor (re_editor
          ignores the app text scale, so it is applied there explicitly).
      
      Fixes
      - File and Git actions never read the server's answer. A 200 carrying
        ok:false — a rejected push, a pull that cannot fast-forward — showed
        "Pushed"/"Pulled", and a refused delete said nothing.
      - Uploads over 48 MB are refused before sending, with the reason.
      - R8 keep rules for printing's android.print bridge; verified in the
        release mapping that PdfConvert and its callbacks keep their names.
      - Version 1.4.0+14; Settings no longer claims 1.0.0.
      
      Tests: test/pdf_export_test.dart (8) — span-balanced listing rows, escaping,
      language detection, download names, and markdown image paths that cannot
      climb out of the project.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      90a71482
  3. 10 Sep, 2026 1 commit
    • Mahmoud Aglan's avatar
      fix(chat): never end a failed run on a green tick · 07dbbf6a
      Mahmoud Aglan authored
      The closing line of every run was drawn green with a check mark regardless of
      outcome. When the server's Claude login lapsed, every message died in 0s and
      still ended "finished in 0s" beside a reassuring green tick — the one place a
      failure most needs to be unmissable.
      
      The footer now takes the message's real status and turns red with an error icon
      when the run failed. Status is authoritative over the footer's wording because
      every run stored before the server learned to write "failed" reads "finished";
      for those, the word is corrected as well as the colour, so the text does not
      contradict the icon next to it.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      07dbbf6a
  4. 06 Sep, 2026 11 commits
    • Mahmoud Aglan's avatar
      feat(chat): plan-first toggle and an approve step · d7ff664f
      Mahmoud Aglan authored
      A toggle in the chat's app bar; when on, every message describes what it would
      do and changes nothing. A finished plan grows two buttons: approve and run it,
      or revise it.
      
      Approving resumes the same Claude session with plan mode off, so the plan is
      context rather than a second question. Revising just seeds the composer, because
      the useful correction is almost always a sentence, not a new prompt.
      
      The approve bar appears only under the newest assistant turn, only when that
      turn actually ran as a plan, and only once it has finished — offering to approve
      something still being written would approve a plan nobody has read.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      d7ff664f
    • Mahmoud Aglan's avatar
      feat: background run watcher and biometric lock · 3c3cb85f
      Mahmoud Aglan authored
      Batch 1 of the 60: the two that change what the app can be trusted with.
      
      BACKGROUND WATCHER finishes a promise the app has implied since the first
      build. The run always continued server-side — the phone is only a window onto
      it — but nothing told you it had landed unless the chat screen happened to be
      open. A foreground service now polls /api/chats and raises a local notification
      when a chat leaves the running set.
      
      Detecting completion by diffing successive polls is the only option available:
      there is no FCM here and the server has nothing to push with. The service seeds
      its set on start, so a run already in flight when it launches is not instantly
      reported as finished. The background isolate cannot see the app's providers, so
      it re-reads the server and token from SharedPreferences and makes its one GET
      with dart:io rather than carrying Dio across the isolate boundary.
      
      Off by default. Android requires a persistent notification for a foreground
      service, and one the user did not ask for is how an app gets uninstalled — so it
      is a switch, set to LOW importance because the ping is the point, not the badge.
      
      BIOMETRIC LOCK, because this app can now push to repositories that deploy to
      clients and redeploy their infrastructure, and a phone left on a table should be
      able to do neither. It re-evaluates on every return to the foreground, not only
      at cold start: someone picking up an already-running phone is the actual risk.
      A grace window avoids prompting on every glance.
      
      Two deliberate choices: the device PIN is accepted as a fallback rather than
      biometrics only, and a PlatformException opens the app instead of blocking it.
      Locking someone out of their own tooling because a fingerprint enrolment was
      lost would be worse than the risk being guarded. This gates the UI; it is not a
      vault, and does not claim to be.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      3c3cb85f
    • Mahmoud Aglan's avatar
      feat(chat): floating jump-to-latest, and honest usage freshness · 7f21d159
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      7f21d159
    • Mahmoud Aglan's avatar
      feat(caprover): sorting, live log viewer — and fix broken per-app URLs · a0d8b3b8
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      a0d8b3b8
    • Mahmoud Aglan's avatar
      feat: CapRover tab, prompt-aware routing, and chat quality-of-life · c50315c4
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      c50315c4
    • Mahmoud Aglan's avatar
      feat(pickers): show what each repo is, not just its name · 66515ead
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      66515ead
    • Mahmoud Aglan's avatar
      fix(chat): make the live feed independent of SSE · 83322120
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      83322120
    • Mahmoud Aglan's avatar
      feat(chat): render prose as prose, tool calls as steps; restore the original mark · 84ee14c6
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      84ee14c6
    • Mahmoud Aglan's avatar
      build: re_editor 0.10.0, and restore Kotlin 2.4.0 · e92220ba
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      e92220ba
    • Mahmoud Aglan's avatar
      build: pin AGP 8.7.3, and generate the flame launcher icon · 483cba44
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      483cba44
    • Mahmoud Aglan's avatar
      feat: Son of Anton — native client for driving Claude against GitLab repos · 0e8a19fa
      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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      0e8a19fa