refactor(coding-agent): simplify extension runtime architecture

- Replace per-extension closures with shared ExtensionRuntime
- Split context actions: ExtensionContextActions (required) + ExtensionCommandContextActions (optional)
- Rename LoadedExtension to Extension, remove setter methods
- Change runner.initialize() from options object to positional params
- Derive hasUI from uiContext presence (no separate param)
- Add warning when extensions override built-in tools
- RPC and print modes now provide full command context actions

BREAKING CHANGE: Extension system types and initialization API changed.
See CHANGELOG.md for migration details.
This commit is contained in:
Mario Zechner 2026-01-07 23:50:18 +01:00
parent faa26ffbf9
commit cb3ac0ba9e
16 changed files with 580 additions and 736 deletions

View file

@ -18,7 +18,7 @@ import type { AgentSession } from "./core/agent-session.js";
import { createEventBus } from "./core/event-bus.js";
import { exportFromFile } from "./core/export-html/index.js";
import { discoverAndLoadExtensions, type ExtensionUIContext, type LoadedExtension } from "./core/extensions/index.js";
import { discoverAndLoadExtensions, type LoadExtensionsResult } from "./core/extensions/index.js";
import type { ModelRegistry } from "./core/model-registry.js";
import { resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage, discoverModels } from "./core/sdk.js";
@ -60,13 +60,11 @@ async function runInteractiveMode(
migratedProviders: string[],
versionCheckPromise: Promise<string | undefined>,
initialMessages: string[],
extensions: LoadedExtension[],
setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
initialMessage?: string,
initialImages?: ImageContent[],
fdPath: string | undefined = undefined,
): Promise<void> {
const mode = new InteractiveMode(session, version, changelogMarkdown, extensions, setExtensionUIContext, fdPath);
const mode = new InteractiveMode(session, version, changelogMarkdown, fdPath);
await mode.init();
@ -236,7 +234,7 @@ function buildSessionOptions(
sessionManager: SessionManager | undefined,
modelRegistry: ModelRegistry,
settingsManager: SettingsManager,
preloadedExtensions?: LoadedExtension[],
extensionsResult?: LoadExtensionsResult,
): CreateAgentSessionOptions {
const options: CreateAgentSessionOptions = {};
@ -302,8 +300,8 @@ function buildSessionOptions(
}
// Pre-loaded extensions (from early CLI flag discovery)
if (preloadedExtensions && preloadedExtensions.length > 0) {
options.preloadedExtensions = preloadedExtensions;
if (extensionsResult && extensionsResult.extensions.length > 0) {
options.preloadedExtensionsResult = extensionsResult;
}
return options;
@ -332,12 +330,12 @@ export async function main(args: string[]) {
time("SettingsManager.create");
// Merge CLI --extension args with settings.json extensions
const extensionPaths = [...settingsManager.getExtensionPaths(), ...(firstPass.extensions ?? [])];
const { extensions: loadedExtensions } = await discoverAndLoadExtensions(extensionPaths, cwd, agentDir, eventBus);
const extensionsResult = await discoverAndLoadExtensions(extensionPaths, cwd, agentDir, eventBus);
time("discoverExtensionFlags");
// Collect all extension flags
const extensionFlags = new Map<string, { type: "boolean" | "string" }>();
for (const ext of loadedExtensions) {
for (const ext of extensionsResult.extensions) {
for (const [name, flag] of ext.flags) {
extensionFlags.set(name, { type: flag.type });
}
@ -347,13 +345,9 @@ export async function main(args: string[]) {
const parsed = parseArgs(args, extensionFlags);
time("parseArgs");
// Pass flag values to extensions
// Pass flag values to extensions via runtime
for (const [name, value] of parsed.unknownFlags) {
for (const ext of loadedExtensions) {
if (ext.flags.has(name)) {
ext.setFlagValue(name, value);
}
}
extensionsResult.runtime.flagValues.set(name, value);
}
if (parsed.version) {
@ -436,7 +430,7 @@ export async function main(args: string[]) {
sessionManager,
modelRegistry,
settingsManager,
loadedExtensions,
extensionsResult,
);
sessionOptions.authStorage = authStorage;
sessionOptions.modelRegistry = modelRegistry;
@ -452,7 +446,7 @@ export async function main(args: string[]) {
}
time("buildSessionOptions");
const { session, extensionsResult, modelFallbackMessage } = await createAgentSession(sessionOptions);
const { session, modelFallbackMessage } = await createAgentSession(sessionOptions);
time("createAgentSession");
if (!isInteractive && !session.model) {
@ -505,8 +499,6 @@ export async function main(args: string[]) {
migratedProviders,
versionCheckPromise,
parsed.messages,
extensionsResult.extensions,
extensionsResult.setUIContext,
initialMessage,
initialImages,
fdPath,