web-to-app
AGENTS.md7.2k starsChanged 23 days agoUnlicenseView on GitHub
Instructions for coding agents working in this repository. Runtime Kotlin is authored under app/ and synced into shell by syncShellRuntimeSources. Edit the app/ source once; do not permanently fork copies under shell. User-facing product docs: README.md, .github/docs/README_CN.md, .github/CONTRIBUTING.md, modules/README.md. The published documentation site is https://shiaho777.github.io/web-to-app/ (source: docs/, deployed by .github/workflows/docs-deploy.yml). Mental model:
We split this AGENTS.md into the rules it gives an agent
42 rules. About 0 are common, 19 are distinctive, and 23 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈19
Distinctive · estimate
This author's own choices. Worth a look.
23
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Distinctive: line 7
Do what you believe is right.
- Distinctive: line 10
Do not add copyright or license headers unless asked.
- Very specific: line 57
Prefer adding properties on the existing split objects (StringsA … StringsE, one object per file).
- Very specific: line 64
Avoid new third-party dependencies unless strongly justified (app/build.gradle.kts / shell/build.gradle.kts).
- Very specific: line 79
Do not feed signed or renamed APKs back into full modifyApk as templates.
- Very specific: line 92
Do not commit secrets, local.properties, keystores, or IDE/cache junk.
- Distinctive: line 93
Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).
- Distinctive: line 94
When changing export or shell packaging, rebuild the template you touched.
- Very specific: line 95
When changing config fields, run checkConfigFieldDrift to catch model ↔ shell config name drift.
- Very specific: line 99
Default target: shiaho777/web-to-app.
- Distinctive: line 103
Links the user can open (required): When you point at an Issue or PR — in chat, in a commit message, or in an Issue/PR body — give the full …
- Very specific: line 110
Do not tell the user to find a finished delivery with is:issue state:open.
- Distinctive: line 112
When the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end.
- Very specific: line 115
If check fails, fix it on the same branch and push.
- Distinctive: line 117
Do not tell the user the delivery is done while the PR is still open, CI is still running or red, or the Issue is still open.
- Distinctive: line 119
Do not close the Issue yourself before the PR is merged and CI is green.
- Very specific: line 121
Branch naming: use plain type/slug names — fix/…, feat/…, refactor/…, docs/…, perf/…, chore/….
- Very specific: line 165
Add the property in the correct Strings* split with all 10 languages.
- Very specific: line 177
Coverage test update (REQUIRED): When adding a new Boolean field to WebViewConfig, you MUST add it to flipAllBooleans() in …
- Distinctive: line 204
Keep changes surgical; shell ships a thin dependency set and a curated source sync.
- Very specific: line 205
If you touch FGS / notification channel creation, fail soft via SafeNotificationChannels.
- Very specific: line 209
Keep implementation under host-only packages (core/apkbuilder, host screens, sample/market, core/host, …).
- Very specific: line 210
Do not pull host-only deps into shell/build.gradle.kts.
- Very specific: line 214
Prefer ApkBuildCache / content hashes over timestamps.
- Distinctive: line 216
Do not use signed outputs as templates.
- Distinctive: line 217
If template bytes change, ensure cache keys invalidate correctly.
- Distinctive: line 221
Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).
- Distinctive: line 222
Do not add OEM push SDKs by default.
- Very specific: line 227
Follow modules/README.md catalog layout (registry.json + module folders).
- Very specific: line 244
Use NetworkModule.downloadClient for large dependency / engine / runtime downloads.
- Very specific: line 269
API signature drift: If the service class constructor, method signature, or return type changes, update the tool's execute() in …
- Distinctive: line 274
Removed features: If a feature is deleted, remove or disable its tool.
- Distinctive: line 293
Copy, don't invent: Before writing any card, open the reference implementation on the same screen and mirror it element by element:
- Distinctive: line 310
Card headers carry title + icon only: No subtitle on the header row, no free-floating description text next to it.
- Very specific: line 311
Expansion state ≠ feature state: Never bind a section's isExpanded / AnimatedVisibility(visible=…) to the feature's enabled flag — …
- Distinctive: line 315
Description text follows the About switch: After the card change, apply the host-UI description rule below to every new subtitle or hint on …
- Distinctive: line 321
After every host UI change — editor cards, dialogs, About, home, not only config cards — look at the new prose and decide:
- Very specific: line 323
Gate it: when it is secondary explanation: a row subtitle, a "what this does" paragraph, a hint under a title.
- Very specific: line 340
Crashing FGS when notification channel creation fails: Always use SafeNotificationChannels for channel creation.
- Distinctive: line 341
Adblock is wired for preview + export: Do not wipe host filter state during export.
- Very specific: line 346
Agent tool ↔ service drift: When a service class API changes, the corresponding Agent tool in core/agent/tool/builtin/ must be updated in …
- Very specific: line 382
Use these when you change shell membership, export packaging, or config fields.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
Instructions for coding agents working in this repository.
Code style
- Do what you believe is right. Make the change complete and correct, not the smallest possible diff. If a fix calls for refactoring, renaming, or touching multiple files, do it.
- Fix root causes, not symptoms. Do not paper over a defect with catch-and-swallow, degraded fallbacks, or UI workarounds that leave the underlying behavior broken. Trace the failure to where it originates and repair it there — even when that is harder.
- Match the patterns and conventions already in the surrounding code.
- Do not add copyright or license headers unless asked.
Project layout
| Path | Role |
app/ | Full builder host: editor UI, export pipeline, runtimes, preview. |
shell/ | Runtime template. Built to app/src/main/assets/template/webview_shell.apk via :shell:assembleRelease + :app:syncShellTemplateApk. |
feature-stacks/ | Optional-dependency modules bundled as graftable packages (dex/ + stack.arsc at package-id 0x80+ + res/ + curated manifest.xml fragment + stack.json). Built by each module's bundle*Stack task, synced to app/src/main/assets/stacks/ by :app:syncStackBundles, injected into generated APKs only when the export config enables the feature (#1115). |
clone-host/ | Host-side APK clone / identity reshape support library. Its DEX asset generation (syncCloneHostDex) is deliberately disabled (enabled = false, AV false-positive mitigation, e0d2d4d6) — AppCloner runs fail-soft without the asset. |
modules/ | Module Market catalog (registry.json + per-module folders). |
sample-bundles/ | Heavy sample dependency packs (python-*-shared.zip + sha256-pinned manifest.json) fetched on demand by SampleSharedPackManager — deliberately NOT in app/assets (saves ~30MB raw / ~7MB compressed from the host APK). Regenerate via scripts/build_sample_bundles.py. |
docs/ | VitePress documentation site (guide / developer / extensions, EN + ZH), published to https://shiaho777.github.io/web-to-app/ by .github/workflows/docs-deploy.yml. Site URL paths map 1:1 to files under docs/ (/zh/... → docs/zh/...). |
scripts/ | Build helpers and gates (check_config_field_drift.py). |
Runtime Kotlin is authored under app/ and synced into shell by syncShellRuntimeSources. Edit the app/ source once; do not permanently fork copies under shell.
User-facing product docs: README.md, .github/docs/README_CN.md, .github/CONTRIBUTING.md, modules/README.md. The published documentation site is https://shiaho777.github.io/web-to-app/ (source: docs/, deployed by .github/workflows/docs-deploy.yml).
How the main pieces connect
Editor (Compose screens in app/)
↔ data models (WebApp, configs)
↔ export factory (ApkConfig / ApkConfigJsonFactory)
↔ ApkBuilder / ApkBuildCache → signed generated APK
app/ sources
→ syncShellRuntimeSources → shell DEX → webview_shell.apk (template)
Generated APK runtime
WebToAppApplication → ShellModeManager → load assets JSON config
→ WebViewManager / runtime servers (Node/PHP/Python/Go/WordPress)Mental model:
- Host preview runs
:appwith all classes on the main classpath. - Generated APK runs the shell template classes (full runtime synced from
app/), reading config from assets JSON viaShellModeManager. - A flag in the editor is useless at export unless it flows through model → ApkConfig JSON → shell config → runtime code.
i18n
- Host UI strings live under
app/src/main/java/com/webtoapp/core/i18n/(facadeobject StringsinStrings.kt, delegating toStringsA…StringsE, one file per split object). - All user-visible strings must be inline
when (Strings.lang)blocks covering all 10 languages: Chinese, English, Arabic, Portuguese, Spanish, French, German, Russian, Japanese, Korean.when(lang)blocks may never useelse ->—AppStringsResourceConsistencyTestandStringsKtTranslationParityTestenforce this. - Never load user-visible text via
context.getString(R.string.*)/stringResource(R.string.*).res/values/strings.xmlholds onlytranslatable="false"resources (e.g.app_name) and no localevalues-*/directories exist, so a localized resource string could never cover the 10 languages and would silently fall back to the defaultvalues/(Chinese). UseStrings.xxx(orStrings.funName(arg)for parameterised strings — seelinuxEnvInstalledToast(name)for the pattern). Tests gate this:kotlin source never references R string for user-visible text, plusvalues strings xml only holds non-localised resourcesandno locale values dirs or grouped app strings files existwhich block resurrecting resource-based strings. R.stringis reserved fortranslatable="false"non-localised resources only (e.g.app_name).- Prefer adding properties on the existing split objects (
StringsA…StringsE, one object per file); match surrounding style. - Shell gets generated string subsets, not the synced files.
syncShellRuntimeSourcesexcludescore/i18n/Strings*.kt;generateShellStrings(→scripts/generate_shell_strings.py) scans the synced runtime sources forStrings.x/StringsX.yreferences (aliasesval S = Stringshandled) and emits reducedStrings.kt/StringsA-E.ktundershell/build/generated/shellStringscarrying only referenced members plus the facade infrastructure — editor-only strings never reach the shell template or generated APKs (~0.75 MB compressed saved per APK). Missing references fail the shell compile loudly; nothing fails silently. Author strings inapp/exactly as before — no extra step needed.
Android and packaging constraints
- Generated apps ship
minSdk23 /targetSdk35 (the shell template).ApkExportConfig.targetSdkcan pin another target (whitelist chips; process-exec types stay 28) and the AAB path forcesDEFAULT_PLAY_TARGET_SDK(36).ApkExportConfig.minSdkcan raise the floor only (23..effectiveTargetSdk); lowering below 23 is rejected because the packed DEX requires API 23. The shell runtime is already adapted to 35 (POST_NOTIFICATIONS gating, FGS types, exact-alarm fallback,enableEdgeToEdge) — keep those paths working if the value moves again. - The host app targets SDK 36 (antivirus reputation) and floors at
minSdk24 — Play rejects a bundle below 24, and both thestandardandgplayflavors share that floor, so the sideloaded APK needs Android 7.0 too. The host floor and the generated-app floor (23) are independent: do not let a generated-appminSdkchange move the host, or vice versa. If the host floor moves, update the builder requirement inREADME.md,.github/docs/README_CN.md, anddocs/{,zh/}guide/{getting-started,faq}.mdin the same change — raising 23 → 24 in #1385 (v81) left all of them stale at "Android 6.0 (API 23)". Host-side exec of app-data binaries is SELinux W^X-blocked, so downloaded toolchains (esbuild, viaNativeNodeEngine) must launch throughHostProcessLauncher(core/linux) — it picks ProcessBuilder where exec is allowed or the memfd static exec bridge (StaticExecProcess/RuntimeExecPolicy) on W^X hosts, and returns a clear error when neither is available. A rawRuntime.exec/ProcessBuilderon a downloaded binary fails on the host (#795). The exec bridge is host-only — generated APKs exec nothing, and the bridge classes are excluded from the shell sync. - Avoid new third-party dependencies unless strongly justified (
app/build.gradle.kts/shell/build.gradle.kts). Prefer platform APIs and existing modules. - Notification push channels: Web Notification polyfill, polling, WebSocket, FCM (developer-owned Firebase config). Do not add OEM vendor push SDKs by default.
- Foreground services and notification helpers must use
SafeNotificationChannels(or equivalent fail-soft create). Channel creation failures must not crash FGS startup. - One shell template:
webview_shell.apkfrom:shellrelease. Do not introduce a second template APK. - Feature stacks (optional deps, #1115): heavy optional SDKs (admob, …) live in
feature-stacks/<id>and graft into the generated APK at export:FeatureStacks.enabledFor(config)gates inclusion,FeatureStackGrafterappendsclassesN.dex+ namespacedres/+ merges the manifest fragment (AxmlRebuilder.mergeManifestFragment) + grafts the stack'sresources.arscpackage (ArscPackageGrafter, package-id ≥ 0x80). Disabled stacks cost zero bytes. Rules for new stacks:- The runtime contract (interface + gates) is authored under
app/so it syncs into the shell; the stack implements it and is reached viaClass.forName+ fail-soft fallback. - Contract types must be name-pinned in
shell/proguard-rules.pro(-keep class com.webtoapp.core.ads.api.** { *; }) — shell R8 renamescom.webtoapp.**, and a renamed interface resolves to the stack's own copy, silently breaking casts. - Contract signatures must carry only framework types or contract-owned types. Kotlin function types (
() -> Unit→kotlin.Function0) compile to library classes that shell R8 renames — the interface method then desugars to a renamed parameter type and the stack impl hitsAbstractMethodError. Use afun interfaceinside the kept api package instead. - Stacks must be self-contained:
stackEmbedGroupsmust cover every non-framework dependency the stack's code touches (gms, ump,org.jetbrains.kotlin). The template's kotlin-stdlib is an R8-processed subset with renamed classes AND inlined helpers — a stack referencingkotlin/jvm/internal/Intrinsicsgets NCDFE/NoSuchMethodError at runtime. Do NOT "fix" it with-keepnamesin shell (names-only doesn't restore inlined methods, and a kept-name partial copy shadows the stack's complete one). mergeManifestFragmentwrites attributes sorted by attribute resource id — PackageParser's TypedArray lookup requires ascending ids and rejects install (INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: <activity> does not specify android:name) even though aapt2 parses the disorder fine.- Stack ids/registry live in
FeatureStacks; bundle sha256 feeds the incrementalmanifestFingerprint.
- The runtime contract (interface + gates) is authored under
- Export incremental rebuild lives in
app/.../apkbuilder(ApkBuildCache+ApkBuilder):- Modes:
FULL/CONTENT_OVERLAY/REUSE_UNSIGNED. - Template / entry identities must be content-stable (no mtime-based keys).
- Encrypted builds always force a full rebuild.
- Do not feed signed or renamed APKs back into full
modifyApkas templates.
- Modes:
- Port coordination:
PortManager+PortConflictMode(REASSIGN/AUTO_KILL/ALERT) with real stop handlers. Local server runtimes must allocate through PortManager and clean up on stop. - Local server / Linux env DNS: fork+exec runtimes (Node / PHP / Python / Go / WordPress / Linux env) should wire through
LocalDnsBridgeProxywhen they need host DNS/proxy bridging. - Large runtime downloads use
NetworkModule.downloadClient(extended timeouts), not the default short-lived client. - HTML / FRONTEND packaged shells need file-scheme access via
ShellWebViewConfig(allowFileAccess/ local-file detection). Do not regress pure file-based HTML loads. - Node.js export must embed
libnode_bridge.so,libnode.so(16KB-aligned), andlibc++_shared.soas native libs. Go export must embedlibgo_exec_loader.so. - C++ STL asymmetry (deliberate): shell builds
c++_staticso the template dropslibc++_shared.soentirely (all shell natives self-contained); the host keepsc++_sharedbecauseinjectNodeJsNativeLibscopieslibc++_shared.sofrom the hostnativeLibraryDirinto NODEJS_APP exports. Do not "unify" the two without rerouting that injection source. - Shell keeps
com.google.android.material: removing the material widgets library from the shell template regressed generated-app launcher icons on Android 16 (rounded source icons rendered as an inner tile over a solid frame; bisected to7c8bdc2f, E-good/F-bad/G-good). The sharedapp/src/main/res/values/themes.xmlreferencesTheme.Material3.*parents and M3 color attrs, which must resolve from the real library at template build time. Do not remove the dep or reintroduce a local compat shim without device-testing generated icons on Android 16. - Shell release is obfuscated (
shell/proguard-rules.pro):com.webtoapp.**members are kept from shrinking but renamed; field names are pinned (<fields>keepclassmembers) so un-annotated Gson model fields keep working. JNI-callback classes and manifest components keep explicit name-preserving rules. Template carries no v1 signature (re-signed at export anyway);apksigner verifyon the raw template fails on that basis alone — verify with--min-sdk-version 24. - Gradle custom tasks (
syncCloneHostDex, etc.) must be configuration-cache safe: captureFile/Providervalues at configuration time, do not referenceProject/android.sdkDirectoryinside task closures.
Workflow
- Do not commit secrets,
local.properties, keystores, or IDE/cache junk. - Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).
- When changing export or shell packaging, rebuild the template you touched.
- When changing config fields, run
checkConfigFieldDriftto catch model ↔ shell config name drift.
Delivery (Issue + PR + CI)
Default target: shiaho777/web-to-app. Prefer a pull request over direct pushes to main when delivering code. Human-facing wording of the same loop lives in CONTRIBUTING.md; keep those docs in sync when this process changes.
Language (required): GitHub Issues and PRs must be written in English — titles, bodies, labels text you author, and delivery comments on the Issue/PR. Local chat with the user may be Chinese or any language; do not copy that language into Issue/PR text.
Links the user can open (required): When you point at an Issue or PR — in chat, in a commit message, or in an Issue/PR body — give the full URL and nothing else glued to it:
https://github.com/shiaho777/web-to-app/issues/Nhttps://github.com/shiaho777/web-to-app/pull/N
Do not write 1367(已关闭), 1367 (closed), #1367 closed, or author:1367. A number plus a status word is not a link. Pasted into the GitHub Issues search box it becomes a qualifier such as author:1367 (已关闭). author: must be a username, so GitHub rejects the filter (Invalid value … for author) and the issues list fails to load. Put status in its own sentence: the link, then "Closed." or "Merged."
Do not tell the user to find a finished delivery with is:issue state:open. That filter hides closed Issues and merged PRs. To look one up, use the URL above, or a search of the number alone (1376) with no author: and no state:open.
When the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end. Opening the PR is not the end of the task. Stay with it until the merge is done:
- Watch the required
checkjob in "Android CI Build" until it finishes.package-apksskips on PRs; do not wait on it. - If
checkfails, fix it on the same branch and push. Do not open a second PR for the fix. - Squash-merge only after
checkis green. Then confirm the Issue is closed by the merge and the head branch is deleted. - Do not tell the user the delivery is done while the PR is still open, CI is still running or red, or the Issue is still open.
Do not close the Issue yourself before the PR is merged and CI is green.
Branch naming: use plain type/slug names — fix/…, feat/…, refactor/…, docs/…, perf/…, chore/…. Do not use tool/agent namespaces (codex/…, devin/…, etc.); the branch belongs to the repo, not the agent.
Shell template and runtime sync
Dual runtime (preview vs export)
Host :app | Generated APK | |
| DEX | All app/src classes | Shell sync include−exclude (full runtime set) |
| Config | Editor / in-memory models | Assets JSON via ShellModeManager |
| Template | Not used | app/src/main/assets/template/webview_shell.apk |
Preview ≠ export unless both paths stay valid:
- Host still has the real implementation.
- Shell-synced code reads the config field from assets JSON at runtime.
- The field name in
ApkConfigJSON matches the@SerializedNameinShellModeManager(Gson silently drops mismatches).
Most common failure: preview works; exported APK silently skips the feature because a config field name drifted between the export factory and the shell config class.
Where each concern is edited
| Concern | Path |
| What enters shell | shell/build.gradle.kts → syncShellRuntimeSources include/exclude |
| Shell string subset | :shell:generateShellStrings → scripts/generate_shell_strings.py |
| Shell template build | :shell:assembleRelease + :app:syncShellTemplateApk |
| Template output | app/src/main/assets/template/webview_shell.apk |
| Config → shell JSON | app/.../apkbuilder/ApkConfigJsonFactory.kt |
| Shell config types | app/.../core/shell/ShellModeManager.kt |
| Runtime WebView config | app/.../ui/shell/ShellWebViewConfig.kt |
| Config drift gate | scripts/check_config_field_drift.py → :app:checkConfigFieldDrift |
| Shell minify policy | shell/proguard-rules.pro |
Common change recipes
These are the default approaches for everyday work. Follow the chain end-to-end; stopping at UI or host-only code is how preview and export diverge.
1. Add or change a host UI string
- Add the property in the correct
Strings*split with all 10 languages. - Reference it from Compose/UI the same way neighbors do.
2. Add an editor setting that must affect the generated APK
Trace and update all of:
- Model (
WebApp/ nested config) and editor UI binding - Export mapping (
ApkBuilder/ApkConfig/ApkConfigJsonFactory) - Shell config types (
ShellModeManager/ shell config data classes) if runtime reads them - Runtime use site in shell-synced code
- Unit tests for export wiring when flags change
- Coverage test update (REQUIRED). When adding a new Boolean field to
WebViewConfig, you MUST add it toflipAllBooleans()inWebViewConfigBooleanCoverageTest.kt. The testflipAllBooleans covers every declared Boolean fielduses reflection to verify that every declared Boolean field is listed — if you forget, CI fails with a message naming the missing field. For non-Boolean fields, add a spot-check tokey non-Boolean WebViewConfig fields survive the export round-trip. This is the safety net that catches "preview works, export broken" before it ships. - Shell UI parity (REQUIRED for per-type player screens). Gallery / media /
splash-style app types render through DIFFERENT composables in preview
(
ui/gallery/,ui/media/) vs generated APKs (ui/shell/). A flag that flows end-to-end still does nothing if the shell screen never reads it (gallery thumbnail bar shipped config + assets correctly and still rendered nothing, #781).ShellUiParityTest.ktenforces this: every model field read by host runtime UI must also be read by shell runtime UI. If you add a field and read it in a host player, the test fails naming the field until you port the behavior to shell or add an explicitallowentry with a reason. When adding a new per-type player pair, extend itspairslist the same way.
Missing any step usually yields: editor shows the switch, export ignores it, or export embeds config the runtime never reads.
3. Change shell runtime behavior used by every generated app
- Edit the source under
app/(shared runtime). - Confirm the file is included by
syncShellRuntimeSources. - Rebuild shell template if you need to validate packaging.
- Keep changes surgical; shell ships a thin dependency set and a curated source sync.
- If you touch FGS / notification channel creation, fail soft via
SafeNotificationChannels.
4. Add a host-only feature (editor, market, tooling)
- Keep implementation under host-only packages (
core/apkbuilder, host screens, sample/market,core/host, …). - Do not pull host-only deps into
shell/build.gradle.kts.
5. Touch APK export or incremental rebuild
- Prefer
ApkBuildCache/ content hashes over timestamps. - Encrypted builds stay full rebuild.
- Do not use signed outputs as templates.
- If template bytes change, ensure cache keys invalidate correctly.
6. Change notifications / engines / network hardening
- Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).
- Do not add OEM push SDKs by default.
- FGS channel create paths must tolerate OEM/channel failures.
7. Module Market / modules/
- Follow
modules/README.mdcatalog layout (registry.json+ module folders). - Runtime consumption still goes through the extension/shell paths if it ships inside generated APKs.
8. Fix "works in preview, broken after export"
Checklist in order:
- Did shell config JSON actually contain the field at runtime?
- Does the field name in
ApkConfigJSON match the@SerializedNamein shell config? RuncheckConfigFieldDrift. - Is the runtime use site shell-synced (not host-only)?
- For adblock: confirm
adBlockEnabledmapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state. - Rebuild template after sync changes (stale template is a frequent miss).
9. Local server runtime / download path
- Allocate ports through
PortManagerwith the configured conflict policy; implement real stop handlers. - Wire fork+exec processes into
LocalDnsBridgeProxywhen they need host DNS/proxy env. - Use
NetworkModule.downloadClientfor large dependency / engine / runtime downloads. - Launch processes through
HostProcessLauncher(core/linux) so W^X hosts (targetSdk ≥ 29) degrade to the user-mode static exec loader instead of crashing.
10. Node.js / Go export
- Node.js: ensure
injectNodeJsNativeLibsembedslibnode_bridge.so+libnode.so(16KB-aligned viaElfAligner16k) +libc++_shared.so. Node binary resolution prefersnativeLibraryDir, falls back to download cache. - Go: ensure
injectGoExecLoaderNativeLibembedslibgo_exec_loader.so. - Go host-side builds never run the multi-process
go builddriver:GoDirectBuilderreplayscompile/asm/linkas single-shot processes throughHostProcessLauncher(user-mode static exec loader on W^X hosts), with a content-keyed persistent package archive cache (#796). NodeServiceruns in a dedicated:nodejsOS process so V8 lifecycle is isolated from the host.
11. Change a feature that has an Agent tool
The in-app Agent exposes up to 60 tools — 55 base + 2 plan-mode + 3 imagery (imagery only load with an image-capable model; see ToolRegistryFactory.build()) — that wrap host service classes. When you change a feature, trace the tool chain:
LLM response (tool_calls)
→ AgentEngine.executeToolCall()
→ PermissionPrompter (write tools ask user; read-only run silently)
→ Tool.execute(args, ctx)
→ Service class (ApkBuilder, AppExporter, PortManager, EngineManager, AdBlocker, …)
→ ToolResult → back to LLMChecklist when touching a feature that an Agent tool wraps:
- API signature drift. If the service class constructor, method signature, or return type changes, update the tool's
execute()inapp/.../agent/tool/builtin/. A stale call compiles only if the old overload still exists; otherwise it's a build break. parametersSchema↔execute()alignment. The JSON schema advertised to the LLM must match whatexecute()actually reads fromargs. Adding a parameter to the service call without exposing it in the schema means the LLM can never pass it.descriptionaccuracy. The LLM decides when and how to call a tool solely fromdescription+parametersSchema. If the feature's behavior changed, update the description text — a misleading description causes silent misuse.isReadOnly()correctness.true→ runs without user confirmation;false→ triggersPermissionPrompterdialog. If a tool gains write side-effects, flip it tofalse.- Registration. New tools must be added to
ToolRegistryFactory.baseTools()(grouped by domain comment). Forgotten registration = tool invisible to the LLM. - Removed features. If a feature is deleted, remove or disable its tool. A tool that calls a dead class crashes at runtime inside the agent loop.
- Host-only. Agent tools live under
core/agent/and are never shell-synced. Do not add them tosyncShellRuntimeSources.
Key paths:
| Concern | Path |
| Tool interface | app/.../agent/tool/Tool.kt |
| Tool registry | app/.../agent/tool/ToolRegistryFactory.kt |
| Tool context (services access) | app/.../agent/tool/ToolContext.kt |
| Built-in tools (by domain) | app/.../agent/tool/builtin/*.kt |
| Agent loop / execution | app/.../agent/engine/AgentEngine.kt |
| Permission prompt (Channel-based) | app/.../agent/permission/PermissionPrompter.kt |
| LLM provider (SSE streaming) | app/.../agent/llm/OpenAiCompatProvider.kt |
12. Change editor config-card UI (Compose)
The editor screens are built from WtaSettingCards that share one visual grammar. The activation-card series (issue #567: PRs #568 → #571 → #573 → #574) took four attempts because each round invented layout instead of copying the neighbours, and each round was only compile-checked. A short prompt like "改一下 XX 卡的 UI" still means: follow these rules.
Copy, don't invent. Before writing any card, open the reference implementation on the same screen and mirror it element by element:
| You are writing… | Copy from (CreateAppWebViewCards.kt) |
| Toggle-headed feature card | FullscreenModeCard |
Collapsible card with WtaChoiceRow header | BrowserAdvancedConfigCard |
| Radio group (single-choice) | PWA offline-strategy selector in sectionOfflinePerformance |
| Sub-toggles / fields inside an expanded zone | staticAssetPack block, proxy block |
| Nested conditional content | proxy MANUAL/PAC AnimatedVisibility swap |
Inputs and dialogs follow the same grammar: text inputs are PremiumTextField, list-manager dialogs are Dialog + WtaCard + embedded TopAppBar (see AdBlockSubscriptionSelectorDialog), and ActivationCodeCard's offline-policy rows are another canonical radio reference.
Hard rules learned the hard way:
- Two sanctioned card shapes. Toggle header:
WtaSettingCard { WtaToggleRow(header); AnimatedVisibility(enabled, CardExpandTransition/CardCollapseTransition) { Column { WtaSectionDivider(); full-bleed rows separated by more dividers } } }. Collapse header:WtaSettingCard { Column { WtaChoiceRow(header); AnimatedVisibility(expanded) { Column(padding(horizontal = WtaSpacing.RowHorizontal, vertical = WtaSpacing.ContentGap), spacedBy(WtaSpacing.ContentGap)) { … } } } }. - Alignment grid.
WtaToggleRow/WtaChoiceRoware full-bleed rows carrying their own 16dp padding. Anything that is not a row — text fields, radios, group labels, buttons, notes — must sit inside aColumn(padding(horizontal = WtaSpacing.RowHorizontal, vertical = WtaSpacing.ContentGap))zone. Sub-toggles inside a zone double-indent (16dp zone + 16dp row); that matches the reference, do not "fix" it. Group labels areText(style = labelMedium, color = primary), notWtaSection. - Radio groups are exactly:
Text(labelMedium, primary)label, thenRow(fillMaxWidth().clip(MaterialTheme.shapes.small).clickable { … }.padding(vertical = 4.dp)) { RadioButton; Spacer(4.dp); Text(bodySmall) }. Never wrap a radio group inWtaSection, never attach per-option prose. - Card headers carry title + icon only. No subtitle on the header row, no free-floating description text next to it. Subtitles belong on child rows that genuinely need one (e.g. 显示状态栏).
- Expansion state ≠ feature state. Never bind a section's
isExpanded/AnimatedVisibility(visible=…)to the feature'senabledflag — expanding a panel must never switch the feature on. Section collapse state is its ownremember { mutableStateOf }. - Conditional sub-blocks (mode swaps, dependent fields) use
AnimatedVisibilitywithCardExpandTransition/CardCollapseTransition, never bareifinside the card body. - Compile ≠ verified. After any card UI change, build + install on the emulator and check the rendered card:
uiautomator dumpelement bounds, compare left edges / row heights of the changed card against its neighbours on the same screen (they must share the same content columns), plus a screenshot pass. Content a few dp off the grid is invisible in code review and obvious on screen. - If a UI rework PR gets "this doesn't match the other cards" feedback, the fix is realignment to these patterns, not further invention.
- Description text follows the About switch. After the card change, apply the host-UI description rule below to every new subtitle or hint on the card.
Host UI descriptions (About → 显示说明文字)
The About screen switch Strings.showDescriptions stores ThemeManager.showDescriptions and is read in Compose as LocalShowDescriptions. It hides helper copy. The default is on, so a missed gate still looks fine until the user turns descriptions off.
After every host UI change — editor cards, dialogs, About, home, not only config cards — look at the new prose and decide:
- Gate it when it is secondary explanation: a row subtitle, a "what this does" paragraph, a hint under a title. Row subtitles use
subtitle = Strings.xxx.ifDescriptionsShown(). A block of explanatory text usesAnimatedVisibility(visible = LocalShowDescriptions.current)orif (LocalShowDescriptions.current), the same way neighbouring screens already do. - Do not gate titles, the control's current value, errors, confirmation buttons, or status that is the only feedback the user has.
ifDescriptionsShownexists so important status stays visible. - Shell / generated-APK UI does not read this preference. It is a host display setting. Do not thread it into shell-synced screens.
Easy-to-miss points
- Keyboard avoidance below API 30 requires the classic window path. Android 10 and lower have no native IME-inset dispatch: an edge-to-edge window (
decorFitsSystemWindows = false) is never resized for the keyboard and reports zero IME insets, so nosoftInputModevalue helps there.WindowHelper.applyImmersiveFullscreentherefore keeps the decor fitting system windows on the RESIZE keyboard path below API 30 (systemSOFT_INPUT_ADJUST_RESIZEworks) and degrades TRANSPARENT/IMAGE status-bar styles to solid colors on that path. Do not re-enable edge-to-edge unconditionally for those devices (#613; #634 flipped only the softInputMode bits and fixed nothing — its Robolectric test passed because it never asserted the layout flags). - Shared sources are authored in
app/. Editing only a file undershell/srcis usually wrong; it will be overwritten on sync or diverge from host. - Config field names drift. Editor model,
ApkConfig, JSON factory, and shell config must stay aligned; Gson silently drops unknown/missing fields. RuncheckConfigFieldDrift. - Shell
targetSdk35 is now the baseline — behavior differences (runtime perms, FGS types, BAL, exact alarms) are already handled in synced code; keep the audit items green when touching shell startup/notifications. - Incremental export cache keys must be content-based; mtime and resigned APKs create false hits/misses.
- HTML/FRONTEND file access. Packaged local-file shells must have
allowFileAccess = true(forced inbuildWebViewBlockandShellWebViewConfig); do not regress pure file-based HTML loads. - Node native libs. Exported NODEJS_APP needs
libnode_bridge.so+libnode.so+libc++_shared.so; missing any causesloadNode/loadJniBridgefailure at runtime. - 16KB page alignment.
libnode.soand other large ELF natives must be 16KB-aligned (ElfAligner16k) for Android 15+ devices;node_bridge.cpp/node_launcher.cenable 16KB app-compat beforedlopen. - Node JNI output bridge.
NodeJniOutputBridgeis a stable class referenced by native code; keep its-keepproguard rule so R8 does not renameonOutput. - Crashing FGS when notification channel creation fails. Always use
SafeNotificationChannelsfor channel creation. - Adblock is wired for preview + export. Do not wipe host filter state during export; the host AdBlocker serves preview and the compiled rule set ships in the APK.
- Runtime permissions are feature-driven.
RuntimePermissionSyncderives the permission list from enabled features; do not revert to a static template. - Splash preview media path. Preview reads splash media from the host filesystem (
splashMediaPath); export packages it into assets. Do not hardcodeassets/splash_media.*as the only source. - Host player UI ≠ shell player UI. Preview players (
ui/gallery/,ui/media/) and generated-APK players (ui/shell/) are separate composables sharing only the config. Port every visible behavior across (thumbnail bar, background, keep-screen-on, orientation) and letShellUiParityTestverify the flags; config flowing end-to-end is necessary but not sufficient. - Port conflict policy. Local server runtimes must allocate through
PortManagerand clean up on stop; do not bind ports directly. - Agent tool ↔ service drift. When a service class API changes, the corresponding Agent tool in
core/agent/tool/builtin/must be updated in the same PR. A stale tool either fails to compile or silently passes wrong arguments at runtime. CheckToolRegistryFactory.baseTools()for the full tool list. - Editor card UI grammar. Config-screen cards share one layout grammar (recipe 12): rows are full-bleed, non-row content sits in 16dp-padded zones, expansion never toggles the feature, and card UI is verified on the emulator — not just compiled.
- About "显示说明文字". Helper copy on host UI is gated by the About switch (
Strings.showDescriptions→ThemeManager.showDescriptions→LocalShowDescriptions). After every host UI change, decide whether new prose is that kind of helper text and gate it if it is. See "Host UI descriptions" under the card-UI recipe.
Forbidden / high-risk mistakes
- Second shell template APK
- Excluding a shell-synced class but leaving imports/constructors in shell-synced sources
- Re-enabling shell R8 obfuscation / aggressive shrink without testing exported apps
- Putting host-only tools back into shell sync for convenience
- OEM push SDKs or unjustified heavy dependencies in shell
- Feeding signed/renamed APKs into template/modify paths
- Crashing FGS when notification channel creation fails
- Committing secrets, keystores, or local machine config
- Regressing HTML/FRONTEND file access in packaged shells
- Shipping a host player-screen feature without its shell counterpart (preview-only UI)
- Shipping NODEJS_APP without
libnode_bridge.so/libnode.so/libc++_shared.so - Skipping 16KB alignment for large ELF natives
- Inventing editor card layout/spacing/animations instead of copying the neighbouring cards' patterns, or shipping card UI verified only by compilation
Verify commands
./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache
./gradlew :app:compileStandardDebugKotlin -x syncCloneHostDex --no-configuration-cache
./gradlew :app:checkConfigFieldDrift --no-configuration-cache
python3 scripts/check_config_field_drift.py
./gradlew --no-configuration-cache -PskipShellTemplateSync=true -Pfdroid=true \
:app:compileStandardReleaseKotlin :shell:compileReleaseKotlin \
:app:processStandardReleaseManifest :shell:processReleaseManifestUse these when you change shell membership, export packaging, or config fields. For host-only UI/string work, targeted compile on :app is usually enough.
CI note: the PR check job compiles only :shell:compileDebugKotlin (debug variant) and skips template sync (-PskipShellTemplateSync=true). The shell release variant (R8 / proguard) and the template pipeline are exercised only by the manual workflow_dispatch packaging job — after touching shell/proguard-rules.pro or shell packaging, run the first command locally before pushing.
Related focused tests often worth running after nearby edits: ApkBuildCacheTest, AdBlockerHostRuntimeTest, AdBlockExportWiringTest, PortManagerTest, BuildInputPreflightTest, GoBuildEnvironmentTest, RuntimePermissionSyncTest.
Implementation snapshot
Landed:
- Single shell template (
webview_shell.apk) from:shellrelease, full runtime synced fromapp/ - Incremental
ApkBuildCache(FULL/CONTENT_OVERLAY/REUSE_UNSIGNED); encrypted builds always full - Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions
SafeNotificationChannelsfail-soft path for FGSPortManagerconflict policies + real stop handlers across Node/PHP/Python/Go/WordPressLocalDnsBridgeProxywiring for local server runtimes (including Node.js)- Runtime downloads via
NetworkModule.downloadClient - Adblock preview + export wiring restored
- HTML/FRONTEND file-access for packaged local shells
- Node.js export:
libnode_bridge.so+ 16KB-alignedlibnode.so+libc++_shared.so; 16KB app-compat before dlopen; stableNodeJniOutputBridgeJNI callback - Go export:
libgo_exec_loader.soembedded; in-app build ENOSPC handling + GOTMPDIR relocation - Go host-side builds on W^X hosts:
GoDirectBuilderreplayscompile/asm/linkas single-shot processes throughHostProcessLauncher(user-mode static exec loader), with a content-keyed persistent package archive cache (#796) - Multi-web: gallery/media site sources embedded into the APK and resolved at runtime; nested site sources degrade to a URL instead of aborting the build (#798, #792)
- Gallery playback: shuffleOnLoop, rememberPosition, overview grid, thumbnail bar, pinch-to-zoom viewer, with host/shell parity enforced by
ShellUiParityTest(#781, #786, #801) - Untrusted bitmap decodes bounded (oversized-image crash fix) (#788)
- Backup/restore coverage aligned with the current storage layout; restarts only when local files change, with throttled progress callbacks (#790, #794)
- NativeBridge enabled by default for newly created apps (#805)
- Runtime permission sync (feature-driven)
- Splash preview media path fallback
- Config field drift detection (
checkConfigFieldDrift) - Module Market: Chrome Web Store live search + GreasyFork browse
- Code editor find-and-replace
- Security hardening sweep: TLS-fingerprint bridge validates upstream certs (system + custom CAs + hostname) and its local CA is signature-verified with no error-type fallback; JS bridges are caller/origin/scheme-gated (NativeBridge CORS bypass, GM bridge, MV3
ChromeHostPermissions); MITM proxy host-allowlisted and CA key wrapped at rest; zip extraction routed throughutil/SafeZip; concurrent exports serialized per package with per-package work dirs; multi-web server-runtime site sources degrade to URL; PHP/Python/WP embed failures fail the build; error pages andTranslateBridgecallbacks JSON-escaped; keystore password sidecars excluded from backups - Agent tool system: up to 60 tools — 55 base + 2 plan-mode + 3 imagery (image-capable models only) — covering app lifecycle, config templates, ports/engine, hosts/runtime, stats/modifier/import, build env/Play, modules, files, and imagery, with Channel-based permission prompting, per-section SSE parse resilience, and plan mode; runtime/build-env tools surface
localExecAllowedso the LLM knows targetSdk>=29 hosts cannot exec app-storage binaries
Trending
Quiet since April.thin dataabout the whole repository
Who's talking about shiahonb777/web-to-app
- @wikitd.bsky.social“Web to App Transform Websites into Android Apps in Seconds …”Bluesky · Mar 9, 2026 · 2 likes
- shiaho777/web-to-app“Add to F-droid store”GitHub · Apr 11, 2026
- shiaho777/web-to-app“获取网站图标功能无效”GitHub · Mar 23, 2026
- FaFre/WebLibre“[Feature request] More PWA options”GitHub · Mar 16, 2026
- RookieEnough/Orion-Data“App Submission [1 App]”GitHub · Jan 22, 2026
The biggest 5 of 5 mentions linking to the repository that our crawl found. Each line is their own headline.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

