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

@ -46,7 +46,7 @@ describe("ExtensionRunner", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const shortcuts = runner.getShortcuts();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("conflicts with built-in"));
@ -79,7 +79,7 @@ describe("ExtensionRunner", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const shortcuts = runner.getShortcuts();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("shortcut conflict"));
@ -108,7 +108,7 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "tool-b.ts"), toolCode("tool_b"));
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const tools = runner.getAllRegisteredTools();
expect(tools.length).toBe(2);
@ -130,7 +130,7 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "cmd-b.ts"), cmdCode("cmd-b"));
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const commands = runner.getRegisteredCommands();
expect(commands.length).toBe(2);
@ -149,7 +149,7 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "cmd.ts"), cmdCode);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const cmd = runner.getCommand("my-cmd");
expect(cmd).toBeDefined();
@ -173,7 +173,7 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "throws.ts"), extCode);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const errors: Array<{ extensionPath: string; event: string; error: string }> = [];
runner.onError((err) => {
@ -199,7 +199,7 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "renderer.ts"), extCode);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const renderer = runner.getMessageRenderer("my-type");
expect(renderer).toBeDefined();
@ -222,7 +222,7 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "with-flag.ts"), extCode);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
const flags = runner.getFlags();
expect(flags.has("--my-flag")).toBe(true);
@ -240,14 +240,13 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "flag.ts"), extCode);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
// Setting a flag value should not throw
runner.setFlagValue("--test-flag", true);
// The flag values are stored in the extension's flagValues map
const ext = result.extensions[0];
expect(ext.flagValues.get("--test-flag")).toBe(true);
// The flag values are stored in the shared runtime
expect(result.runtime.flagValues.get("--test-flag")).toBe(true);
});
});
@ -261,7 +260,7 @@ describe("ExtensionRunner", () => {
fs.writeFileSync(path.join(extensionsDir, "handler.ts"), extCode);
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
const runner = new ExtensionRunner(result.extensions, tempDir, sessionManager, modelRegistry);
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
expect(runner.hasHandlers("tool_call")).toBe(true);
expect(runner.hasHandlers("agent_end")).toBe(false);