Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/update-libs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jobs:
["rainbow-on-the-go"]="rainbow-on-the-go.cgp"
["ai-literacy-course"]="ai-literacy-course.cgp"
["flutter-template"]="flutter-template.cgp"
["get-ai-models"]="get-ai-models.cgp"
)
for module in "${!MAP[@]}"; do
src=$(ls "${module}/build/plugin/"*.cgp 2>/dev/null | head -n1)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`code-suggestions-plugin/`](code-suggestions-plugin/) | Inline ghost-text code completions powered by AI. |
| [`speech-to-text-plugin/`](speech-to-text-plugin/) | Voice-to-code: converts speech to code with AI generation. |
| [`vector-search-plugin/`](vector-search-plugin/) | Semantic code search using embeddings and vector similarity. |
| [`get-ai-models/`](get-ai-models/) | Bottom-drawer catalog of curated, fully-open GGUF model files; downloads one to `/sdcard/Download` and verifies its SHA-256. |

## Building a plugin

Expand Down
35 changes: 35 additions & 0 deletions get-ai-models/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Log/OS Files
*.log
.DS_Store

# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.aab
*.apk
output-metadata.json

# IntelliJ
*.iml
.idea/

# Keystore / signing material — never commit
*.jks
*.keystore
release.properties
signing.properties
keystore.properties

# Google Services
google-services.json

# Android Profiling
*.hprof
167 changes: 167 additions & 0 deletions get-ai-models/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Get AI Models

A Code On The Go plugin that adds a **Get AI Models** tab to the project editor's bottom drawer. The
tab lists a bundled catalog of small GGUF language-model files; each row downloads one `.gguf`
to `/sdcard/Download` and verifies its SHA-256.

**Download only.** Loading or running a model is the job of a separate AI plugin built around
llama.cpp. This plugin never loads a model and never checks the device's RAM. It does keep one small
record — which entries passed the checksum — so a verified file is not forgotten on restart; see
[Remembering what is verified](#remembering-what-is-verified).

## Build

```sh
cd get-ai-models
../gradlew assemblePlugin # release .cgp -> build/plugin/get-ai-models.cgp
../gradlew assemblePluginDebug # debug variant
../gradlew test # re-validates the shipped catalog asset
```

Uses the shared repo-root Gradle wrapper and the shared repo-root `libs/` jars — it carries no
wrapper or jars of its own. A `local.properties` containing `sdk.dir=<path-to-android-sdk>` is
required.

Install the resulting `.cgp` through Code On The Go's Plugin Manager, then open a project and look in
the editor's bottom drawer.

## Supported IDE versions

`26.29`–`26.31` (`plugin.min_ide_version` / `plugin.max_ide_version`) — covers the current stable
release and the two before it.

## What a row shows

Each row is **one file** — a model at one quantization — so the same model can appear more than once.

- Compact line: `parameters · quantization · on-disk size`
- Tap to expand: strengths-and-weaknesses paragraph, publisher, minimum RAM, licence, context window
- Minimum RAM is informational only; the device's memory is never read

## What happens on Download

1. **No connection** → blocked with a message, nothing queued.
2. **Metered connection** → warned with the file size; proceed or cancel.
3. **Transfer** → Android's `DownloadManager`. The Download button is replaced by a **Cancel**
control whose background fills as bytes arrive, and the status line reads e.g.
`Downloading 1.4 GB of 4.68 GB`. Tapping it cancels and discards the partial file.
4. **Paused is reported, not hidden.** A download started on an unmetered link will not roll over
onto cellular by itself — it pauses, and the row says `Paused — waiting for Wi-Fi`. Same for
`no connection` and `retrying`.
5. **Verification** → the finished file is streamed and hashed. Match: kept in `/sdcard/Download`.
Mismatch: **deleted**, with a re-download offered.

Transfers and verification are plugin-scoped, so closing the tab mid-download does not abandon
either.

### Approved deviation: progress on the row

The ticket's locked decision is *"progress shown in the system notification... No in-app per-row
progress bar."* The row does show progress — as a fill behind the Cancel control — so **this is a
deviation, not a compliant reading of that rule.**

It was requested and approved after on-device testing: the system notification is dismissible, and
once dismissed the row could not distinguish a slow download from one paused indefinitely waiting for
Wi-Fi. There is no separate progress-bar widget, but that is a mitigation, not compliance.

## Remembering what is verified

A passed checksum is recorded in the plugin's own SharedPreferences (`entryId → {absolutePath,
sizeBytes, verifiedAt}`), because hashing costs seconds to minutes and its result is not cheaply
re-derivable. On activation each record is revalidated with **one `stat`** — a remembered badge is
never shown without checking the disk still agrees:

| On disk | Row shows |
| --- | --- |
| Path gone | `Download` — the record is dropped (you deleted or moved the file) |
| Present, size matches the record | `Downloaded` |
| Present, size differs | `Verify` — the checksum no longer holds, so it is not claimed |

The size check catches every truncated or interrupted file for free. The one case it cannot see is a
file swapped for a *different* file of identical length, so an expanded row offers **Verify file
again**, which re-hashes on demand. A failed re-verify **does not delete** the file — it was not
written by this download, so removing it is the user's call, unlike the hard delete on a failed
*download*.

### When Android reports no path

Everything above needs the finished download's absolute path, which comes from DownloadManager's
`COLUMN_LOCAL_URI`. That column is not guaranteed to be a `file://` URI — on newer platforms it can be
`content://downloads/all_downloads/<id>`, whose path (`/all_downloads/42`) is **not** a filesystem
path. Resolution therefore runs in three tiers: the `file://` URI, then `COLUMN_LOCAL_FILENAME` (which
still carries the real path), and finally the `content://` URI on its own.

The third tier still verifies — the bytes are hashed through a `ContentResolver` — but there is no path
to record or delete, so that row shows `Downloaded` for the session, says the location is unknown, and
offers neither **Verify file again** nor **Delete file**. A checksum failure is still deleted, via
`DownloadManager.remove()`, which does not need a path. The download is never simply failed for want of
a path, which is what a naive `File(uri.path)` produced.

### Approved deviation: this record is a model registry

The ticket lists *"any 'installed' state, model registry"* as out of scope. This record is one, so it
is a deviation — **requested and approved by the ticket owner**, not an author decision, after a
verified 4 GB download lost its `Downloaded` badge on every IDE restart while the file sat on disk.
Keeping the badge without revalidating it would instead have made it lie once the file was deleted.

It stays small and bounded: one JSON string per entry, cleared on uninstall. **Delete (below) extends
this same deviation** — it is what the record makes possible — rather than opening a new one. The
neighbouring exclusion, *"handling pre-existing files"*, is still honoured: nothing without a record
is ever read, verified, or deleted.

## Deleting a downloaded file

An expanded row with a recorded download also offers **Delete file** (red, destructive), which frees
the space and returns the row to `Download`. It asks first, naming the file and its full path.

The safety rule: it deletes **only the recorded absolute path** — a file this plugin wrote and hashed
— never a path composed from the catalogued file name. A same-named file you put in
`/sdcard/Download` yourself is never a candidate, which also keeps the plugin out of "handling
pre-existing files". The record is dropped only once the file is actually gone, so a failed delete
leaves the row claiming what is still true. Deletion runs on the IO dispatcher and is refused while a
verification is hashing.

Two caveats: if an AI plugin currently has the model loaded, the row clears but the space is not
reclaimed until that plugin releases the mapping; and the system Downloads list can keep showing the
file until Android rescans the folder.

## Catalog

A static JSON asset (`src/main/assets/catalog/models.json`). No remote catalog — a revised model list
ships as a new plugin version.

The ticket's hard gates are: instruction-tuned; fully open licence verified on both the base model
and the GGUF re-upload; loads and generates coherent output in our AI plugin; passes the fixed
tool-use eval at ≥ 80%; valid chat template; single file from a reputable uploader with the SHA-256
pinned to a specific revision; context window ≥ 16k tokens. Quantization is *not* restricted.

**The current catalog does not satisfy all of them.** The maintainer selected these six models
explicitly, waiving the instruction-tuned, open-licence and 16k-context gates for specific entries —
so `CatalogLoader` no longer enforces the licence or context gates, and the unit tests no longer
assert them. `docs/CURATION.md` records exactly which gate each entry breaks.

Version 1.0.0 ships 6 files, all under 1 GB: Qwen2.5 0.5B (base), Qwen3 0.6B, Qwen3.5 0.8B,
Llama 3.2 1B Instruct, SmolLM2 360M Instruct, and H2O Danube3 500M (base). Five are Apache-2.0;
Llama 3.2 1B is under the Llama 3.2 Community Licence.

**The two behavioural gates are not yet proven.** Per the ticket, the admission harness that runs a
candidate through the AI plugin is downstream work, so the shipped catalog is the set of candidates
clearing every statically checkable gate. See [`docs/CURATION.md`](docs/CURATION.md) for the
per-entry evidence, the rejected candidates and why, and the procedure for adding an entry.

## In-IDE help

Long-press the tab, any row, or the Download / Cancel / Verify / Delete controls for Tier 1/Tier 2
tooltips (six entries); the tab's tooltip links to the full offline guide bundled at
`src/main/assets/docs/index.html`. Registered through `DocumentationExtension` under the category
`plugin_org.appdevforall.getaimodels`.

## Permissions

`network.access` (the HTTPS fetch), `filesystem.read` (re-read the download to hash it), and
`filesystem.write` (delete a download that fails the checksum, or one the user asks to remove). No
native code, no system commands, no IDE settings, no project-structure access.

## Full documentation

[`get-ai-models.html`](get-ai-models.html) — overview, architecture, and rationale.
86 changes: 86 additions & 0 deletions get-ai-models/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.itsaky.androidide.plugins.build")
}

pluginBuilder {
pluginName = "get-ai-models"
}

android {
namespace = "org.appdevforall.getaimodels"
compileSdk = 36

defaultConfig {
applicationId = "org.appdevforall.getaimodels"
minSdk = 21
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

// The unit tests parse src/main/assets/catalog/models.json off the filesystem, so the
// JVM test task needs the module directory as its working directory.
testOptions {
unitTests.all {
it.workingDir = projectDir
}
}

packaging {
resources {
excludes += setOf(
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt"
)
}
}
}

dependencies {
compileOnly(files("../libs/plugin-api.jar"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlin.stdlib)

testImplementation(libs.junit)
testImplementation(libs.json) // real org.json for JVM unit tests (android.jar's is stubbed)
testImplementation(files("../libs/plugin-api.jar"))
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

// The application-as-library packaging these plugins use trips the AAR metadata checks.
tasks.matching {
it.name.contains("checkDebugAarMetadata") ||
it.name.contains("checkReleaseAarMetadata")
}.configureEach {
enabled = false
}
Loading