Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@
-keep class com.google.android.material.** { *; }
-keep class io.noties.markwon.** { *; }
-keep class com.google.gson.** { *; }
-keep class org.snakeyaml.engine.** { *; }

-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
Expand Down
37 changes: 35 additions & 2 deletions docs/PLUGIN_API_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,39 @@ need a source change, a recompile, or both · `tooling` = API-stability
milestone. **[verified]** = read from the checked-in ABI dump. **[reconstructed]**
= diffed from `plugin-api/src` history (predates the dump; symbol-accurate).

### 26.40 — unreleased
### 26.41 — unreleased
- **added — The AI prompt config engine and settings-pane helpers** _(ADFA-6281)_ **[verified]**
Every AI plugin carried its own copy of the code that reads and renders its prompt
config, and the credential screens their own copy of the reveal toggle and pane
styling, so a fix had to be repeated per plugin and a missed copy made the plugins
drift. The host now ships one copy.
`com.itsaky.androidide.plugins.ai.prompt`: `PromptTemplateEngine` and `PromptText`
(the `{{NAME}}` / `{{#NAME}}` / `{{^NAME}}` renderer; names may be in any case and hold
dots, e.g. `{{fileName}}` or `{{item.name}}`, and config text keeps its whitespace as YAML
parsed it); `PromptConfigLoader.load(source,
parser)`, which reads `agent.yml` and its `include` list off the main thread;
`PromptConfigDocument` and `PromptConfigObject`, the strict key-by-key reader a parser
maps the merged YAML through; `PromptConfigSource` / `AssetPromptConfigSource`;
`PromptConfigException`; and `PromptConfigStore<T>`, the per-activation cache, behind
`PromptConfigProvider<T>`. Loader and store are generic over the plugin's config type:
a plugin supplies only a `PromptConfigParser<T>` and keeps one store, e.g.
`val shared = PromptConfigStore(MyParser)`, and calls `shared.reload(source, onLoaded, onFailed)`
from `activate()` and `shared.clear()` from `deactivate()`. The YAML library (snakeyaml-engine 2.10)
is on the host side, so a plugin using the loader no longer bundles it.
`com.itsaky.androidide.plugins.ai.ui`: `SecretRevealController`, whose two states are
each a `RevealToggle` (icon and content description), and
`View.applyPaneStyling(PaneStyle, outlinedButtonIds)`, with `PaneStyle` grouping a
`ButtonColors` per emphasis and a `FieldColors`. Both take the plugin's own resource ids
rather than shipping any: they resolve against the view's context, which carries the
plugin's resources, not the host's.
Additive to the ABI (183 added lines in the dump, none removed), but no longer unused:
AI-Core and the Gemini, Local and OpenAI agents now load and render their prompt config
through `ai.prompt` and drop their private copies, and the Gemini, OpenAI and MCP
settings screens use `ai.ui`. `LlmInferenceService.WebSearchBackend` (`canSearchWeb()`)
lets a backend say whether a `web_search` request would be searched now; ai-core forces
and offers its `web_search` tool only when it does. Floor
`plugin.min_ide_version` at `26.41` to use any of it; an older IDE has none of these
classes, and the plugin fails with `NoClassDefFoundError` on first use.
- **added — Read-only App Logs and IDE Logs** _(ADFA-6267)_ **[verified]**
Plugins could read build output (`IdeBuildService.getBuildOutput()`) but not the App Logs
or IDE Logs tabs, so an agent diagnosing a runtime crash had to ask the user to paste them.
Expand All @@ -50,7 +82,8 @@ milestone. **[verified]** = read from the checked-in ABI dump. **[reconstructed]
`LogReadResult.EMPTY`, never a throw. The service has no clear or write method, and needs
no permission: plugins run in-process under the IDE's uid, so a gate would disclose log
access, not enforce it. Purely additive (the ABI dump diff is additions only). Floor
`plugin.min_ide_version` at `26.40` to use it; an older IDE has no such service.
`plugin.min_ide_version` at `26.41` to use it; an older IDE has no such service.
### 26.40 — 2026-09-29
- **added — An embedding capability a backend can declare** _(ADFA-6053)_ **[verified]**
A backend that has an embedding model can now say so. The only embedding entry point
before this was `LlmInferenceService.getEmbeddings(String, String)`, which addresses a
Expand Down
2 changes: 1 addition & 1 deletion docs/plugin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The surface a plugin binds to is broader than one module. All of the following a
- Extension interfaces plugins **implement**: `UIExtension`, `EditorExtension`, `EditorTabExtension`, `DocumentationExtension`, `BuildActionExtension`, `SnippetExtension`, `ProjectExtension`, `FileOpenExtension`, `SettingsExtension`.
- IDE service interfaces plugins **call** (via `ServiceRegistry.get(X::class.java)`): `IdeProjectService`, `IdeEditorService`, `IdeFileService`, `IdeEnvironmentService`, `IdeArchiveService`, `IdeBuildService`, `IdeLogService`, `IdeUIService`, `IdeEditorTabService`, `IdeTooltipService`, `IdeThemeService`, `IdeFeatureFlagService`, `IdeCommandService`, `IdeTemplateService`, `IdeSnippetService`, `IdeSidebarService`.
- Cross-plugin service interfaces, where **one plugin implements what another calls** (via `SharedServices`): `LlmInferenceService` — implemented by ai-core, called by every AI plugin — together with the types nested in it that a *backend* plugin implements (`LlmBackend`, `HistoryCapableBackend`, `ToolCallingBackend`, `CancellableBackend`, `ConfigurableBackend`, `EmbeddingBackend`) and the value types either side constructs (`ChatMessage`, `LlmConfig`, `LlmResponse`, `SystemPromptRequest`, `ToolDefinition`, `ToolCallRequest`). Also `ToolSourceRegistry` — implemented by ai-core, called by any plugin contributing tools to the agent — with `ToolSource` and `ToolSpec`, which a *contributing* plugin implements, `ToolInvocation`, which ai-core constructs and passes to `ToolSource.invoke`, and `ToolOutcome`, which the source returns.
- Utility classes plugins **instantiate**: `KeystoreSecretStore` (AES/GCM over the Android Keystore, alias supplied by the caller). Host-side implementation rather than an interface, so plugins share one copy in the process instead of compiling their own.
- Utility classes plugins **instantiate**: `KeystoreSecretStore` (AES/GCM over the Android Keystore, alias supplied by the caller); the AI prompt config engine in `ai.prompt` (`PromptTemplateEngine`, `PromptConfigLoader`, `PromptConfigStore`, `PromptConfigDocument`, `PromptConfigObject`, `AssetPromptConfigSource`), generic over a plugin's own config type through `PromptConfigParser` and `PromptConfigProvider`, which a plugin implements; and the settings-pane helpers in `ai.ui` (`SecretRevealController` with `RevealToggle`, `applyPaneStyling` with `PaneStyle`, `ButtonColors` and `FieldColors`). Host-side implementations rather than interfaces, so plugins share one copy in the process instead of compiling their own.
- Data classes plugins **construct** (e.g. `MenuItem`, `TabItem`, `EditorTabItem`, `NavigationItem`, `ToolbarAction`, `FabAction`, `PluginBuildAction`, `SnippetContribution`, `PluginTooltipEntry`, `PluginSettingsEntry`).
- Enums / sealed types plugins **reference**: `PluginPermission`, `ShowAsAction`, `ArchiveFormat`, `BuildActionCategory`, `ToolbarActionIds`, `CommandSpec`, `CommandResult`, `ExtractResult`, `KeystoreSecretStore.Stored`. Sealed, so a plugin `when`s over the cases exhaustively — adding one is a **breaking** change, not an additive one.
- **Wire/format contracts outside the module:**
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ tooling-agp = { module = "com.android.tools.build:gradle", version.ref = "agp-to
tooling-builderModel = { module = "com.android.tools.build:builder-model", version.ref = "agp-tooling" }
tooling-gradleApi = { module = "com.itsaky.androidide.gradle:gradle-tooling-api", version.ref = "gradle-tooling" }
tooling-slf4j = { module = "org.slf4j:slf4j-api", version = "2.0.12" }
# 2.x targets Java 8; 3.x needs 11. Match the AI plugins' copy, which the app shadows parent-first.
snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version = "2.10" }

# Classpaths
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
Expand Down
183 changes: 183 additions & 0 deletions plugin-api/api/plugin-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,185 @@ public abstract interface class com/itsaky/androidide/plugins/ServiceRegistry {
public abstract fun unregister (Ljava/lang/Class;)V
}

public final class com/itsaky/androidide/plugins/ai/prompt/AssetPromptConfigSource : com/itsaky/androidide/plugins/ai/prompt/PromptConfigSource {
public static final field Companion Lcom/itsaky/androidide/plugins/ai/prompt/AssetPromptConfigSource$Companion;
public static final field DEFAULT_ROOT Ljava/lang/String;
public fun <init> (Landroid/content/res/AssetManager;)V
public fun <init> (Landroid/content/res/AssetManager;Ljava/lang/String;)V
public synthetic fun <init> (Landroid/content/res/AssetManager;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun read (Ljava/lang/String;)Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/prompt/AssetPromptConfigSource$Companion {
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptConfigDocument {
public static final field Companion Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigDocument$Companion;
public synthetic fun <init> (Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun fileOf (Ljava/lang/String;)Ljava/lang/String;
public final fun getEntryFile ()Ljava/lang/String;
public final fun getValues ()Ljava/util/Map;
public final fun read (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptConfigDocument$Companion {
public final fun merge (Ljava/lang/String;Ljava/util/List;)Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigDocument;
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptConfigException : java/lang/IllegalArgumentException {
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptConfigLoader {
public static final field FILE Ljava/lang/String;
public static final field INSTANCE Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigLoader;
public final fun load (Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigSource;Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigParser;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun load$default (Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigLoader;Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigSource;Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigParser;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptConfigObject {
public final fun int (Ljava/lang/String;)I
public final fun invalid (Ljava/lang/String;Ljava/lang/String;)Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigException;
public final fun labelOf (Ljava/lang/String;)Ljava/lang/String;
public final fun obj (Ljava/lang/String;)Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigObject;
public final fun objectEntries (Ljava/lang/String;)Ljava/util/Map;
public final fun objects (Ljava/lang/String;)Ljava/util/List;
public final fun optionalTextEntries (Ljava/lang/String;)Ljava/util/Map;
public final fun read (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public final fun text (Ljava/lang/String;)Lcom/itsaky/androidide/plugins/ai/prompt/PromptText;
public final fun texts (Ljava/lang/String;)Ljava/util/List;
}

public abstract interface class com/itsaky/androidide/plugins/ai/prompt/PromptConfigParser {
public abstract fun parse (Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigDocument;)Ljava/lang/Object;
}

public abstract interface class com/itsaky/androidide/plugins/ai/prompt/PromptConfigProvider {
public abstract fun config (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class com/itsaky/androidide/plugins/ai/prompt/PromptConfigSource {
public abstract fun read (Ljava/lang/String;)Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptConfigStore : com/itsaky/androidide/plugins/ai/prompt/PromptConfigProvider {
public fun <init> (Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigParser;)V
public final fun clear ()V
public fun config (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun configIfLoaded ()Ljava/lang/Object;
public final fun preload (Lkotlinx/coroutines/CoroutineScope;Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigSource;)Lkotlinx/coroutines/Deferred;
public final fun reload (Lcom/itsaky/androidide/plugins/ai/prompt/PromptConfigSource;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Deferred;
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptTemplateEngine {
public static final field FIRST Ljava/lang/String;
public static final field INSTANCE Lcom/itsaky/androidide/plugins/ai/prompt/PromptTemplateEngine;
public static final field LAST Ljava/lang/String;
public final fun render (Lcom/itsaky/androidide/plugins/ai/prompt/PromptText;Ljava/util/Map;)Ljava/lang/String;
public final fun render (Ljava/lang/String;Ljava/util/Map;)Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptTemplateEngine$RenderException : java/lang/IllegalArgumentException {
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public final class com/itsaky/androidide/plugins/ai/prompt/PromptText {
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/itsaky/androidide/plugins/ai/prompt/PromptText;
public static synthetic fun copy$default (Lcom/itsaky/androidide/plugins/ai/prompt/PromptText;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/itsaky/androidide/plugins/ai/prompt/PromptText;
public fun equals (Ljava/lang/Object;)Z
public final fun getLabel ()Ljava/lang/String;
public final fun getTemplate ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/ui/ButtonColors {
public fun <init> (IILjava/lang/Integer;Ljava/lang/Integer;)V
public synthetic fun <init> (IILjava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()I
public final fun component2 ()I
public final fun component3 ()Ljava/lang/Integer;
public final fun component4 ()Ljava/lang/Integer;
public final fun copy (IILjava/lang/Integer;Ljava/lang/Integer;)Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;
public static synthetic fun copy$default (Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;IILjava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;
public fun equals (Ljava/lang/Object;)Z
public final fun getContainer ()Ljava/lang/Integer;
public final fun getContent ()I
public final fun getRipple ()I
public final fun getStroke ()Ljava/lang/Integer;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/ui/FieldColors {
public fun <init> (IIII)V
public final fun component1 ()I
public final fun component2 ()I
public final fun component3 ()I
public final fun component4 ()I
public final fun copy (IIII)Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;
public static synthetic fun copy$default (Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;IIIIILjava/lang/Object;)Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;
public fun equals (Ljava/lang/Object;)Z
public final fun getEndIcon ()I
public final fun getError ()I
public final fun getHint ()I
public final fun getStroke ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/ui/PaneStyle {
public fun <init> (Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;IIII)V
public final fun component1 ()Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;
public final fun component2 ()Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;
public final fun component3 ()Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;
public final fun component4 ()I
public final fun component5 ()I
public final fun component6 ()I
public final fun component7 ()I
public final fun copy (Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;IIII)Lcom/itsaky/androidide/plugins/ai/ui/PaneStyle;
public static synthetic fun copy$default (Lcom/itsaky/androidide/plugins/ai/ui/PaneStyle;Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;IIIIILjava/lang/Object;)Lcom/itsaky/androidide/plugins/ai/ui/PaneStyle;
public fun equals (Ljava/lang/Object;)Z
public final fun getButtonStrokeWidth ()I
public final fun getCornerRadius ()I
public final fun getDivider ()I
public final fun getDividerThickness ()I
public final fun getField ()Lcom/itsaky/androidide/plugins/ai/ui/FieldColors;
public final fun getFilledButton ()Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;
public final fun getOutlinedButton ()Lcom/itsaky/androidide/plugins/ai/ui/ButtonColors;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/ui/PaneStyling {
public static final fun applyPaneStyling (Landroid/view/View;Lcom/itsaky/androidide/plugins/ai/ui/PaneStyle;Ljava/util/Set;)V
}

public final class com/itsaky/androidide/plugins/ai/ui/RevealToggle {
public fun <init> (II)V
public final fun component1 ()I
public final fun component2 ()I
public final fun copy (II)Lcom/itsaky/androidide/plugins/ai/ui/RevealToggle;
public static synthetic fun copy$default (Lcom/itsaky/androidide/plugins/ai/ui/RevealToggle;IIILjava/lang/Object;)Lcom/itsaky/androidide/plugins/ai/ui/RevealToggle;
public fun equals (Ljava/lang/Object;)Z
public final fun getDescription ()I
public final fun getIcon ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/itsaky/androidide/plugins/ai/ui/SecretRevealController {
public fun <init> (Lcom/google/android/material/textfield/TextInputLayout;Landroid/widget/EditText;Lcom/itsaky/androidide/plugins/ai/ui/RevealToggle;Lcom/itsaky/androidide/plugins/ai/ui/RevealToggle;Lkotlin/jvm/functions/Function1;)V
public final fun attach ()V
public final fun isRevealed ()Z
public final fun mask ()V
}

public abstract interface annotation class com/itsaky/androidide/plugins/base/InternalPluginApi : java/lang/annotation/Annotation {
}

Expand Down Expand Up @@ -1690,6 +1869,10 @@ public abstract interface class com/itsaky/androidide/plugins/services/LlmInfere
public abstract fun onToolCall (Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ToolCallRequest;)V
}

public abstract interface class com/itsaky/androidide/plugins/services/LlmInferenceService$WebSearchBackend : com/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend {
public abstract fun canSearchWeb ()Z
}

public final class com/itsaky/androidide/plugins/services/LogEntry {
public fun <init> (Lcom/itsaky/androidide/plugins/services/LogLevel;Ljava/lang/String;)V
public final fun component1 ()Lcom/itsaky/androidide/plugins/services/LogLevel;
Expand Down
Loading
Loading