Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9aec661
feat: add code-suggestions plugin
jatezzz Jul 15, 2026
d15fd29
feat: add speech-to-text plugin
jatezzz Jul 15, 2026
52b9f24
feat: add vector-search plugin
jatezzz Jul 15, 2026
61da30e
fix(ai-core): load the user-selected model instead of newest .gguf in…
jatezzz Jul 15, 2026
d9ee1c2
fix(ai-core): reject embedding models on the chat path instead of cra…
jatezzz Jul 15, 2026
f136bdd
fix(ai-assistant): fetch live Gemini models and drop retired gemini-1…
jatezzz Jul 15, 2026
c2b7f08
fix(ai-assistant): settings dialog UX and backend selection bugs
jatezzz Jul 15, 2026
57aca4d
fix(code-suggestions-plugin): LLM service lookup
jatezzz Jul 15, 2026
69290da
feat(vector-search): wire semantic project search, also fixed code-su…
jatezzz Jul 15, 2026
6e414e1
refactor: Address AI plugin review feedback
jatezzz Jul 16, 2026
b307c0b
fix(ai-core): shut down Llm-RunLoop executor on plugin dispose
jatezzz Jul 16, 2026
74d53d1
refactor: add icons day and night for AI plugins
jatezzz Jul 17, 2026
48b41fc
fix(ai-assistant): surface LLM-not-configured feedback and follow IDE…
jatezzz Jul 21, 2026
f9ebb1c
refactor: extract hardcoded strings to strings.xml and improve user f…
jatezzz Jul 22, 2026
b939ff5
fix(ai-plugins): resolve PR review findings across the AI plugin suite
jatezzz Jul 23, 2026
0ee5f9c
fix(ai-plugins): honor selected backend and resolve Gemini OkHttp crash
jatezzz Jul 23, 2026
8f2477f
chore(ai-plugins): align IDE version range, docs, and icons across AI…
jatezzz Jul 24, 2026
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`ai-core/`](ai-core/) | Shared on-device LLM inference backend (bundled llama.cpp AAR) plus a Gemini API backend, exposed to other plugins as a runtime service. |
| [`ai-assistant/`](ai-assistant/) | In-IDE AI chat assistant with tool calling; talks to `ai-core` for inference over local or Gemini models. |
| [`flutter-template/`](flutter-template/) | Adds Flutter starter project templates (Basic, BLoC, Provider, GetX, Riverpod) to the New Project screen. |
| [`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. |

## Building a plugin

Expand Down
16 changes: 16 additions & 0 deletions ai-assistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ The build resolves `plugin-api.jar` from the repo-root `../libs/`.
- `fragments/AiSettingsFragment.kt`, `viewmodel/AiSettingsViewModel.kt` — model/backend config
- `tool/` — the agent tool-loop (executor, router, per-tool handlers, approval)

## Security

- **Gemini API key at rest.** When you use the Gemini backend, the API key is
stored in this plugin's private `SharedPreferences` (`AgentSettings`). That
store is **app-sandboxed** — other apps cannot read it — but it is **not
encrypted at rest**, so it is recoverable on a rooted or otherwise compromised
device. Remove it any time from **AI Settings** (or `clearGeminiApiKey()`).
Encryption is deliberately not layered on here: the key is shared at runtime
with the sibling `ai-core` plugin (which performs the Gemini calls), and
`EncryptedSharedPreferences` would force both independent plugins to agree on a
master-key alias and crypto library version — a fragile cross-plugin coupling.
A host-provided secure-storage service is the correct long-term fix.
- **Gemini API key in transit.** The key is sent to Google as an `x-goog-api-key`
request header (and via the SDK on the chat path), never in a URL query string.
- **File tools are confined to the project root** — see the in-IDE help page.

## License

GPL-3.0 — same as AndroidIDE / CodeOnTheGo.
7 changes: 2 additions & 5 deletions ai-assistant/ai-assistant.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ <h2>1. Executive Overview</h2>
<li><b>AI Core</b> — the inference engine. Provides an
<code>LlmInferenceService</code> with two backends: a local, on-device
model (llama.cpp) and Google Gemini. Install this first.</li>
<li><b>AI Assistant</b> — the user interface. Contributes the Agent tab, the
editor context-menu actions, and the agent tool-loop that drives the
model.</li>
<li><b>AI Assistant</b> — the user interface. Contributes the Agent tab and
the agent tool-loop that drives the model.</li>
</ul>
<p>The two plugins are bridged by a shared service registry
(<code>SharedServices</code>), so the UI plugin discovers the inference
Expand All @@ -71,8 +70,6 @@ <h2>2. Core Functionality</h2>
and generate code from templates.</li>
<li><b>Dual inference backends</b> — fully offline on-device inference, or
Gemini in the cloud, selectable in Settings.</li>
<li><b>Editor context actions</b> — <b>Explain Code</b> and
<b>Generate Code</b> from the editor selection.</li>
<li><b>Safety controls</b> — filesystem tools are confined to the project
root, and mutating tools require explicit user approval.</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion ai-assistant/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

<meta-data
android:name="plugin.max_ide_version"
android:value="26.28" />
android:value="26.30" />

<!-- Number of editor tabs contributed via getEditorTabs() (the "Agent" tab) -->
<meta-data
Expand Down
4 changes: 0 additions & 4 deletions ai-assistant/src/main/assets/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ <h2>What the agent can do</h2>
device.
</div>

<h2>Context-menu actions</h2>
<p>Select code in the editor and open the context menu for
<b>Explain Code</b> and <b>Generate Code</b> shortcuts.</p>

<h2>Troubleshooting</h2>
<ul>
<li><b>"No model configured"</b> — select a <code>.gguf</code> file (Local)
Expand Down
Binary file modified ai-assistant/src/main/assets/icon_day.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ai-assistant/src/main/assets/icon_night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.itsaky.androidide.plugins.aiassistant
import com.itsaky.androidide.plugins.IPlugin
import com.itsaky.androidide.plugins.PluginContext
import com.itsaky.androidide.plugins.extensions.UIExtension
import com.itsaky.androidide.plugins.extensions.ContextMenuContext
import com.itsaky.androidide.plugins.extensions.DocumentationExtension
import com.itsaky.androidide.plugins.extensions.MenuItem
import com.itsaky.androidide.plugins.extensions.PluginTooltipButton
Expand All @@ -20,8 +19,20 @@ class AiAssistantPlugin : IPlugin, UIExtension, DocumentationExtension {
private var llmService: LlmInferenceService? = null

companion object {
/** Must match `plugin.id` in AndroidManifest.xml — keys the host's plugin Context lookup
* used by [com.itsaky.androidide.plugins.base.PluginFragmentHelper.getPluginInflater]. */
const val PLUGIN_ID = "com.itsaky.androidide.plugins.aiassistant"

/** Tooltip category for this plugin (strict `plugin_<pluginId>` convention); shared by the tab and the AI Settings screen. */
const val TOOLTIP_CATEGORY = "plugin_$PLUGIN_ID"

const val TOOLTIP_TAG_TAB = "agent_chat_tab"

// Tags for the interactive controls on the AI Settings dialog (see AiSettingsFragment).
const val TOOLTIP_TAG_SETTINGS_BACKEND = "ai_settings_backend"
const val TOOLTIP_TAG_SETTINGS_LOCAL_MODEL = "ai_settings_local_model"
const val TOOLTIP_TAG_SETTINGS_GEMINI_KEY = "ai_settings_gemini_key"

@Volatile
private var pluginContext: PluginContext? = null

Expand Down Expand Up @@ -87,30 +98,6 @@ class AiAssistantPlugin : IPlugin, UIExtension, DocumentationExtension {
)
}

override fun getContextMenuItems(menuContext: ContextMenuContext): List<MenuItem> {
val selectedText = menuContext.selectedText
if (selectedText.isNullOrBlank()) {
return emptyList()
}

return listOf(
MenuItem(
id = "ai_explain_code",
title = "Explain Code",
isEnabled = true,
isVisible = true,
action = { context.logger.info("Explain Code clicked") }
),
MenuItem(
id = "ai_generate_code",
title = "Generate Code",
isEnabled = true,
isVisible = true,
action = { context.logger.info("Generate Code clicked") }
)
)
}

override fun getMainMenuItems(): List<MenuItem> = emptyList()

// --- DocumentationExtension: three-tier tooltip help for the Agent tab ---
Expand All @@ -120,7 +107,7 @@ class AiAssistantPlugin : IPlugin, UIExtension, DocumentationExtension {
// Tier 3 = `buttons[].uri` (offline HTML page served from
// src/main/assets/docs/ at localhost)

override fun getTooltipCategory(): String = "plugin_ai_assistant"
override fun getTooltipCategory(): String = TOOLTIP_CATEGORY

override fun getTooltipEntries(): List<PluginTooltipEntry> = listOf(
PluginTooltipEntry(
Expand All @@ -147,6 +134,50 @@ class AiAssistantPlugin : IPlugin, UIExtension, DocumentationExtension {
order = 0
)
)
),
PluginTooltipEntry(
tag = TOOLTIP_TAG_SETTINGS_BACKEND,
summary = "Choose which model powers the Agent: on-device Local (llama.cpp) or cloud Gemini.",
detail = """
<p>Selects the active inference backend:</p>
<ul>
<li><b>Local</b> — runs a <code>.gguf</code> model entirely on
the device; nothing leaves the phone.</li>
<li><b>Gemini</b> — calls Google's cloud API over HTTPS; needs
an API key.</li>
</ul>
<p>The choice below changes which settings appear.</p>
""".trimIndent(),
buttons = listOf(
PluginTooltipButton(description = "AI Assistant guide", uri = "index.html", order = 0)
)
),
PluginTooltipEntry(
tag = TOOLTIP_TAG_SETTINGS_LOCAL_MODEL,
summary = "Pick a local .gguf chat model to run on-device.",
detail = """
<p>Browse for a <code>.gguf</code> model file to load with
llama.cpp. Use a <b>chat/instruct</b> model — embedding-only
models can't generate replies. Larger models are slower and use
more memory; the file is copied into the app's private storage on
first use.</p>
""".trimIndent(),
buttons = listOf(
PluginTooltipButton(description = "AI Assistant guide", uri = "index.html", order = 0)
)
),
PluginTooltipEntry(
tag = TOOLTIP_TAG_SETTINGS_GEMINI_KEY,
summary = "Enter your Google Gemini API key. It is stored only on this device.",
detail = """
<p>Paste a Gemini API key to enable the cloud backend. The key is
kept in this plugin's private preferences on-device and is sent
only to Google's API over HTTPS. Requests (your prompts and
project context) leave the device when Gemini is selected.</p>
""".trimIndent(),
buttons = listOf(
PluginTooltipButton(description = "AI Assistant guide", uri = "index.html", order = 0)
)
)
)

Expand Down Expand Up @@ -229,8 +260,6 @@ class AiAssistantPlugin : IPlugin, UIExtension, DocumentationExtension {
}
}

// Note: Encrypted Gemini API key migration handled by EncryptedPrefs

if (migratedCount > 0) {
context.logger.info("Migrated $migratedCount settings from app to plugin")
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import java.util.Date
import java.util.Locale

class ChatAdapter(
private val pluginContext: Context,
private val markwon: Markwon,
private val onMessageAction: (action: String, message: ChatMessage) -> Unit
) : ListAdapter<ChatMessage, RecyclerView.ViewHolder>(DiffCallback) {
Expand Down Expand Up @@ -84,8 +83,8 @@ class ChatAdapter(

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
android.util.Log.d("ChatAdapter", "onCreateViewHolder called, viewType=$viewType")
// Use plugin context for inflating layouts to access plugin resources
val inflater = LayoutInflater.from(pluginContext)
// Inflate from the RecyclerView's Context so item views follow the IDE day/night theme.
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_SYSTEM -> {
val view = inflater.inflate(R.layout.list_item_chat_system_message, parent, false)
Expand Down
Loading