Skip to content

Commit 2a8eff5

Browse files
authored
ADFA-6281 | Share AI prompt template engine and pane helpers (#2087)
* feat(plugin-api): ADFA-6281 share AI prompt engine and pane helpers Host-side template engine, generic YAML config loader/store and UI helpers, consumed by AI-Core and the Gemini, Local, OpenAI and MCP agents in plugin-examples. ABI-additive. Prompt config: - Include paths refuse a backslash and ':', which a Windows host would read as a separator or a drive. - Any IOException from a source is reported as a PromptConfigException naming the file. - Duplicate keys are refused explicitly; already SnakeYAML Engine's default, stated so an upgrade cannot flip it. - Config text keeps its whitespace as YAML parsed it, so `|` keeps its final newline and `|-` drops it. - PromptConfigStore treats a load that completed with an exception as failed: preload() retries it and configIfLoaded() returns null rather than calling getCompleted(). Template engine: - Names may be in any case and hold dots ({{fileName}}, {{item.name}}), looked up as the whole key. Pane helpers: - SecretRevealController keeps the user's selection across a toggle, does not report legibility from attach(), is @mainthread, and waits for the redraw via postOnAnimation. - applyPaneStyling does not recurse into a styled control's children. * fix(plugin-api): ADFA-6281 address review on prompt engine and pane helpers Accept padded tags and reject malformed ones, re-mask on reattach, floor changelog at 26.41. * feat(plugin-api): ADFA-6281 add WebSearchBackend for live web search capability Backends implement canSearchWeb() so ai-core offers web_search only when a search would run. * feat(plugin-api): ADFA-6281 add PromptConfigStore.reload Owns the load scope and reports load/failure, so plugins drop their copied lifecycle code. * fix(plugin-api): ADFA-6281 address second review round - PromptConfigStore.reload: catch and log a throwing callback instead of letting it reach the uncaught handler; document the callback thread; serialize reload and clear. - PromptTemplateEngine: `{{{{` renders a literal `{{`. - SecretRevealController: onLegibleChanged KDoc covers re-attach. - LlmInferenceService: list WebSearchBackend among the capabilities. * feat(plugin-api): define web_search and required_tool extraParams keys Refs: ADFA-6281
1 parent 2c09a2c commit 2a8eff5

22 files changed

Lines changed: 2177 additions & 4 deletions

‎app/proguard-rules.pro‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@
256256
-keep class com.google.android.material.** { *; }
257257
-keep class io.noties.markwon.** { *; }
258258
-keep class com.google.gson.** { *; }
259+
-keep class org.snakeyaml.engine.** { *; }
259260

260261
-keep class com.google.firebase.** { *; }
261262
-keep class com.google.android.gms.** { *; }

‎docs/PLUGIN_API_CHANGELOG.md‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,41 @@ need a source change, a recompile, or both · `tooling` = API-stability
3535
milestone. **[verified]** = read from the checked-in ABI dump. **[reconstructed]**
3636
= diffed from `plugin-api/src` history (predates the dump; symbol-accurate).
3737

38-
### 26.40 — unreleased
38+
### 26.41 — unreleased
39+
- **added — The AI prompt config engine and settings-pane helpers** _(ADFA-6281)_ **[verified]**
40+
Every AI plugin carried its own copy of the code that reads and renders its prompt
41+
config, and the credential screens their own copy of the reveal toggle and pane
42+
styling, so a fix had to be repeated per plugin and a missed copy made the plugins
43+
drift. The host now ships one copy.
44+
`com.itsaky.androidide.plugins.ai.prompt`: `PromptTemplateEngine` and `PromptText`
45+
(the `{{NAME}}` / `{{#NAME}}` / `{{^NAME}}` renderer; names may be in any case and hold
46+
dots, e.g. `{{fileName}}` or `{{item.name}}`, `{{{{` writes a literal `{{`, and config text keeps its whitespace as YAML
47+
parsed it); `PromptConfigLoader.load(source,
48+
parser)`, which reads `agent.yml` and its `include` list off the main thread;
49+
`PromptConfigDocument` and `PromptConfigObject`, the strict key-by-key reader a parser
50+
maps the merged YAML through; `PromptConfigSource` / `AssetPromptConfigSource`;
51+
`PromptConfigException`; and `PromptConfigStore<T>`, the per-activation cache, behind
52+
`PromptConfigProvider<T>`. Loader and store are generic over the plugin's config type:
53+
a plugin supplies only a `PromptConfigParser<T>` and keeps one store, e.g.
54+
`val shared = PromptConfigStore(MyParser)`, and calls `shared.reload(source, onLoaded, onFailed)`
55+
from `activate()` and `shared.clear()` from `deactivate()`. The YAML library (snakeyaml-engine 2.10)
56+
is on the host side, so a plugin using the loader no longer bundles it.
57+
`com.itsaky.androidide.plugins.ai.ui`: `SecretRevealController`, whose two states are
58+
each a `RevealToggle` (icon and content description), and
59+
`View.applyPaneStyling(PaneStyle, outlinedButtonIds)`, with `PaneStyle` grouping a
60+
`ButtonColors` per emphasis and a `FieldColors`. Both take the plugin's own resource ids
61+
rather than shipping any: they resolve against the view's context, which carries the
62+
plugin's resources, not the host's.
63+
Additive to the ABI (185 added lines in the dump, none removed), but no longer unused:
64+
AI-Core and the Gemini, Local and OpenAI agents now load and render their prompt config
65+
through `ai.prompt` and drop their private copies, and the Gemini, OpenAI and MCP
66+
settings screens use `ai.ui`. `LlmInferenceService.WebSearchBackend` (`canSearchWeb()`)
67+
lets a backend say whether a `web_search` request would be searched now; ai-core forces
68+
and offers its `web_search` tool only when it does. The `extraParams` keys both sides
69+
read are defined once, as `WebSearchBackend.EXTRA_PARAM_WEB_SEARCH` and
70+
`ToolCallingBackend.EXTRA_PARAM_REQUIRED_TOOL`. Floor
71+
`plugin.min_ide_version` at `26.41` to use any of it; an older IDE has none of these
72+
classes, and the plugin fails with `NoClassDefFoundError` on first use.
3973
- **added — Read-only App Logs and IDE Logs** _(ADFA-6267)_ **[verified]**
4074
Plugins could read build output (`IdeBuildService.getBuildOutput()`) but not the App Logs
4175
or IDE Logs tabs, so an agent diagnosing a runtime crash had to ask the user to paste them.
@@ -50,7 +84,8 @@ milestone. **[verified]** = read from the checked-in ABI dump. **[reconstructed]
5084
`LogReadResult.EMPTY`, never a throw. The service has no clear or write method, and needs
5185
no permission: plugins run in-process under the IDE's uid, so a gate would disclose log
5286
access, not enforce it. Purely additive (the ABI dump diff is additions only). Floor
53-
`plugin.min_ide_version` at `26.40` to use it; an older IDE has no such service.
87+
`plugin.min_ide_version` at `26.41` to use it; an older IDE has no such service.
88+
### 26.40 — 2026-09-29
5489
- **added — An embedding capability a backend can declare** _(ADFA-6053)_ **[verified]**
5590
A backend that has an embedding model can now say so. The only embedding entry point
5691
before this was `LlmInferenceService.getEmbeddings(String, String)`, which addresses a

‎docs/plugin-api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The surface a plugin binds to is broader than one module. All of the following a
1313
- Extension interfaces plugins **implement**: `UIExtension`, `EditorExtension`, `EditorTabExtension`, `DocumentationExtension`, `BuildActionExtension`, `SnippetExtension`, `ProjectExtension`, `FileOpenExtension`, `SettingsExtension`.
1414
- 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`.
1515
- 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.
16-
- 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.
16+
- 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.
1717
- Data classes plugins **construct** (e.g. `MenuItem`, `TabItem`, `EditorTabItem`, `NavigationItem`, `ToolbarAction`, `FabAction`, `PluginBuildAction`, `SnippetContribution`, `PluginTooltipEntry`, `PluginSettingsEntry`).
1818
- 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.
1919
- **Wire/format contracts outside the module:**

‎gradle/libs.versions.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ tooling-agp = { module = "com.android.tools.build:gradle", version.ref = "agp-to
299299
tooling-builderModel = { module = "com.android.tools.build:builder-model", version.ref = "agp-tooling" }
300300
tooling-gradleApi = { module = "com.itsaky.androidide.gradle:gradle-tooling-api", version.ref = "gradle-tooling" }
301301
tooling-slf4j = { module = "org.slf4j:slf4j-api", version = "2.0.12" }
302+
# 2.x targets Java 8; 3.x needs 11. Match the AI plugins' copy, which the app shadows parent-first.
303+
snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version = "2.10" }
302304

303305
# Classpaths
304306
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }

0 commit comments

Comments
 (0)