> aiaw-mobile-fork-8-new-files-277-diffs-one-phone

AIaW Mobile Fork: 8 New Files, 277 Diffs, One Phone
// ─────────────────────────────────────────────────────────────
OUTPUT 003 SEED 627919108 DATE 2026-05-12 CHARS 14,420 TAGS vue3 capacitor ios android mcp pyodide pdfjs typescript ai-client localfs

What I forked

AIaW (originally NitroRCr/AIaW) is an elegant AI chat client. The upstream is web-first, has no camera, no real iOS support, no conversation export, and ships with cloud sync enabled. None of that is what I wanted on a phone.

The fork is two months of work: 245 commits by me (335 total when counting merges and bot bumps, Apr 25 – Jun 27, 2026), 11 verified releases (v2.0.8 through v2.0.8.11), Quasar + Vue 3 + Capacitor, 27,957 lines of source in src/, plus 424 lines of native Android Java. The whole diff against upstream/v1.6: 277 files changed, 15,099 insertions, 3,752 deletions. Shipped APK: 13.7 MB (14,320,027 bytes). Repo on disk: 456 MB (.git) + 1.4 GB iOS Xcode cache.

What follows are the features. Eight of them. Each one tied to a real file in the fork, not a category in my head.

1. MCP plugin refresh — PR #1

3a75e26 shipped MCP refresh, camera capture, and retry dialog in one PR. Upstream could install an MCP plugin but had no way to re-pull the manifest when the source changed — the plugin would silently keep using the stale tool list until you uninstalled and reinstalled.

Added a refresh button to each MCP plugin in the installed list, plus a refreshMcpPlugin(id) action in the plugin store:

async function refreshMcpPlugin(id: string) {
  const plugin = await db.installedPluginsV2.where('id').equals(id).first()
  if (!plugin || plugin.type !== 'mcp') throw new Error('Not an MCP plugin')
  const dump = await dumpMcpPlugin(plugin.manifest)
  await db.installedPluginsV2.update(plugin.key, { manifest: dump })
}

MCP got another six follow-up commits after that — make provider settings optional, expose them in plugin config, hide unsupported plugins from the mobile market, then four rounds of tighten iOS MCP gating and restore token visibility toggle. Mobile + MCP is where most of the breakage was.

2. Native camera in the composer

Upstream had a web file picker only — on mobile, attaching a photo meant leaving the app, opening the camera, saving, coming back, finding the file. Native camera puts a camera button next to the image button. Tap → iOS/Android camera → photo lands in the conversation.

The work was 28 commits and it was mostly the failure modes:

  • First-launch camera permission denial needs a retry dialog with explanation.
  • Camera button placement: pinned to the left of the image button so it's reachable one-handed.
  • WebView's default long-press on images was triggering system vibration and a gesture deadlock on Android — the app would freeze. Disabled.

3. LocalFs native plugin — a three-layer stack

This is the most ambitious single piece of native code in the fork. It is not one file. It's three:

  1. android/app/src/main/java/app/aiaw/LocalFsPlugin.java — 424 lines of Capacitor plugin code, registered under the name LocalFs. 11 native methods, all backed by Android's Storage Access Framework (SAF). It uses ACTION_OPEN_DOCUMENT_TREE with FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION | FLAG_GRANT_PREFIX_URI_PERMISSION so the user-picked tree URI survives app restarts. Renames try DocumentsContract.renameDocument first, fall back to DocumentFile.renameTo for SAF providers that don't implement it. Reads return offset / returnedChars / totalChars / hasMore so JS can paginate a 50,000-char default. Moves are copy + delete (non-atomic; the code rejects if the source delete fails after a successful copy — no half-moved state without warning).
  2. src/utils/local-fs-native.ts — 84 lines. The TS bridge: registerPlugin<LocalFsPlugin>('LocalFs'), the full TypeScript interface, and a localStorage key aiaw_localfs_dir_mount_v1 that persists the current mounted directory.
  3. src/utils/local-fs-native-plugin.ts — 178 lines. This is what the LLM actually calls. It wraps the TS bridge as 13 AI tool methods (mountDir / getMountedDir / remountDir / unmountDir / listDir / readFile / writeFile / mkdir / createFile / delete / rename / copy / move), each with its own @sinclair/typebox parameter schema and a system prompt describing the tool to the model.

Why three layers: the Java does the actual ContentResolver.openInputStream work; the TS bridge gives you type safety in app code; the AI tool wrapper is what the model sees in its function-calling manifest. They are three different audiences.

There is a fourth file in the same area, src/utils/file-ops-plugin.ts (354 lines), which is a parallel AI tool set using capacitor-plugin-shell-exec instead of SAF, with a localStorage-backed sandbox index (aiaw_file_index_v2). It's the "shell" path; SAF is the "Android-native" path. Both exist because some file operations are easier one way and some the other.

4. Pyodide — Python in the WebView

src/utils/code-exec-plugin.ts, 183 lines. Runs Python in the browser via Pyodide (WebAssembly), no backend needed. The plugin loads https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js on first use, waits up to 10 seconds for window.loadPyodide to become available, then calls loadPackage(['micropip', 'numpy', 'matplotlib', ...]). The LLM gets a single exec tool with two parameters: code and packages (extra Pyodide packages to load).

It works on a phone. The first call is slow (Pyodide download + init); subsequent calls are fast because the runtime is cached in module state (_pyodide, _loadPromise, _scriptPromise). It's also preloaded in index.html with <script src="...pyodide.js" defer> so the user sees the warm path.

5. Image cache for short-TTL MCP outputs

src/utils/image-cache.ts, 165 lines. MCP servers sometimes return image URLs that expire fast (signed S3 links, ephemeral generated images). If you click one in a chat, the URL might be dead by the time the user looks. This layer fixes that.

Flow: injectImageCache() scans rendered DOM for <img> with external src. For each one, fetch the image, store the ArrayBuffer in a Dexie/IndexedDB table, replace the img.src with a blob: URL (in-memory, valid for page lifetime). The click handler on the viewer receives the cached arrayBuffer directly, so the download button works even after the original URL is gone. Two Maps (original → blob URL, blob URL → original) and a third Map for in-flight fetches (so two <img> tags with the same source don't double-fetch). Only http(s):// URLs are cached; data:, blob:, and relative paths are skipped.

6. Document parsing — doc-parse-plugin + pdf.js + fflate

Three files, one feature: read PDFs and Office documents inside the app.

  • src/utils/doc-parse-plugin.ts (248 lines) — the AI tool. POSTs the file to a configured DocParseBaseURL/parse endpoint with optional language and target_pages, returns the extracted text. Uses fflate to handle zipped Office formats.
  • src/utils/pdf.ts (112 lines) — the PDF reader. pdfjs-dist@5.2.133 with isEvalSupported: false and useSystemFonts: true. Worker loaded from unpkg.com (no bundling). Exports getDocumentProxy, isPDFDocumentProxy, extractText (with optional mergePages).
  • src/utils/doc-parse.ts — removed. The old doc-parse utility (40 lines) was deleted; replaced by the plugin.

7. Vendored code editor (codejar)

src/utils/codejar.ts, 579 lines. This is a vendored copy of antonmedv/codejar — you can tell because the file starts with // from https://github.com/antonmedv/codejar and a top-of-file /* eslint-disable */. It is the code-editor used by the doc-parse and shell-exec features when they need to show users a query they can edit. Not a code change of mine; just shipped in the fork so the build doesn't need a network call to a CDN for it.

8. Cerebras + MiniMax as first-class providers

Both providers became first-class on the same level as OpenAI / Anthropic. Provider auto-match also removed the hardcoded model list — providers now self-describe their models.

Per-provider work:

  • Cerebras — openai-compatible but needs fallback schema; model list endpoint can fail, added manual entry; key history isolated per provider with per-key notes.
  • MiniMax — multiple think blocks after tool calls need extraction; if a stream ends with think-only output, synthesize a final answer; allow more tool steps than default; clean output before persisting.

What else shipped (the smaller items)

  • AI SDK middlewares (src/utils/middlewares.ts, 84 lines) — three new middlewares: FormattingReenabled (re-injects the system prompt after a tool result erases formatting intent), MarkdownFormatting (forces backticks for file paths, \(/\[ for math), AuthropicCors (sets anthropic-dangerous-direct-browser-access: true so the browser can hit Anthropic without a backend proxy).
  • Token history dropdown for API key fields — avoids the iOS secure keyboard coming up every time, with per-provider isolation and notes.
  • Password visibility toggle for API key inputs (5 commits, including backup/restore).
  • Custom GlobalToast system replacing Quasar's default — swipe-to-dismiss, success/fail colors, 1.2s auto-dismiss, full save path in the caption.
  • Cloud sync disabled by default. Hosted-service login removed from onboarding. Local-first only.
  • DialogView refactor: 2364 → 1250 lines, split into five composables (useDialogChain, useDialogScroll, useDialogInput, branch control, artifact helpers). 42 commits. Architecturally the biggest change in the fork.

What didn't ship

  • Web release. The fork ships iOS and Android. quasar dev still works for contributors; verified releases are mobile only.
  • Tauri build. The upstream had started one. Code is still there but I didn't maintain it; my work was all Capacitor.
  • iOS LocalFs. The Android plugin is native Java. iOS would need a Swift equivalent; I didn't write it. iOS users get the file picker through Capacitor's standard mechanisms instead.

What I learned writing this

The first draft of this post was wrong in two ways. First, the numbers: I quoted 255 commits and 27,198 lines and 7.9 MB from memory. The actual git diff --stat upstream/v1.6..origin/master says 245 / 27,957 / 13.7 MB. Second, the framing: I organized it as "refactoring categories" — DialogView split, Provider refactor, etc. That's how the fork got built, not what the fork is. Nobody installs a fork for its dialog composables. They install it for the camera button, the file picker, the refresh-on-the-MCP-list, the Python-in-the-WebView, the export button that doesn't crash.

The right unit of work for a post like this is the file, not the commit. Eight new files in src/utils/ + one in android/app/src/main/java/, each tied to a feature. That is the fork.

Try it

Self-host the fork and build for iOS/Android via Capacitor:

git clone https://github.com/lingion/AIaW.git
cd AIaW
npm install
npx quasar dev          # web
npx cap add ios         # iOS
npx cap add android     # Android

Star on GitHub · Releases

See also: AIaW Operation Docs — step-by-step user guide covering install, architecture, providers, plugins, workspaces, and mobile platforms.

← cd .. /