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:
android/app/src/main/java/app/aiaw/LocalFsPlugin.java— 424 lines of Capacitor plugin code, registered under the nameLocalFs. 11 native methods, all backed by Android's Storage Access Framework (SAF). It usesACTION_OPEN_DOCUMENT_TREEwithFLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION | FLAG_GRANT_PREFIX_URI_PERMISSIONso the user-picked tree URI survives app restarts. Renames tryDocumentsContract.renameDocumentfirst, fall back toDocumentFile.renameTofor SAF providers that don't implement it. Reads returnoffset / returnedChars / totalChars / hasMoreso 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).src/utils/local-fs-native.ts— 84 lines. The TS bridge:registerPlugin<LocalFsPlugin>('LocalFs'), the full TypeScript interface, and alocalStoragekeyaiaw_localfs_dir_mount_v1that persists the current mounted directory.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/typeboxparameter 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 configuredDocParseBaseURL/parseendpoint with optionallanguageandtarget_pages, returns the extracted text. Usesfflateto handle zipped Office formats.src/utils/pdf.ts(112 lines) — the PDF reader.pdfjs-dist@5.2.133withisEvalSupported: falseanduseSystemFonts: true. Worker loaded fromunpkg.com(no bundling). ExportsgetDocumentProxy,isPDFDocumentProxy,extractText(with optionalmergePages).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
thinkblocks 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(setsanthropic-dangerous-direct-browser-access: trueso 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
GlobalToastsystem 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.
DialogViewrefactor: 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 devstill 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
See also: AIaW Operation Docs — step-by-step user guide covering install, architecture, providers, plugins, workspaces, and mobile platforms.