1. 06 Sep, 2026 4 commits
    • 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