fix(coding-agent): add replacement-session callbacks closes #2860

This commit is contained in:
Mario Zechner
2026-04-22 12:13:54 +02:00
parent a900d25119
commit 1cc303d053
11 changed files with 579 additions and 67 deletions

View File

@@ -121,6 +121,12 @@ export function createExtensionRuntime(): ExtensionRuntime {
const notInitialized = () => {
throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
};
const state: { staleMessage?: string } = {};
const assertActive = () => {
if (state.staleMessage) {
throw new Error(state.staleMessage);
}
};
const runtime: ExtensionRuntime = {
sendMessage: notInitialized,
@@ -140,6 +146,12 @@ export function createExtensionRuntime(): ExtensionRuntime {
setThinkingLevel: notInitialized,
flagValues: new Map(),
pendingProviderRegistrations: [],
assertActive,
invalidate: (message) => {
state.staleMessage ??=
message ??
"This extension instance is stale after session replacement or reload. Use the provided replacement-session context instead.";
},
// Pre-bind: queue registrations so bindCore() can flush them once the
// model registry is available. bindCore() replaces both with direct calls.
registerProvider: (name, config, extensionPath = "<unknown>") => {
@@ -167,12 +179,14 @@ function createExtensionAPI(
const api = {
// Registration methods - write to extension
on(event: string, handler: HandlerFn): void {
runtime.assertActive();
const list = extension.handlers.get(event) ?? [];
list.push(handler);
extension.handlers.set(event, list);
},
registerTool(tool: ToolDefinition): void {
runtime.assertActive();
extension.tools.set(tool.name, {
definition: tool,
sourceInfo: extension.sourceInfo,
@@ -181,6 +195,7 @@ function createExtensionAPI(
},
registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void {
runtime.assertActive();
extension.commands.set(name, {
name,
sourceInfo: extension.sourceInfo,
@@ -195,6 +210,7 @@ function createExtensionAPI(
handler: (ctx: import("./types.js").ExtensionContext) => Promise<void> | void;
},
): void {
runtime.assertActive();
extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
},
@@ -202,6 +218,7 @@ function createExtensionAPI(
name: string,
options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
): void {
runtime.assertActive();
extension.flags.set(name, { name, extensionPath: extension.path, ...options });
if (options.default !== undefined && !runtime.flagValues.has(name)) {
runtime.flagValues.set(name, options.default);
@@ -209,77 +226,95 @@ function createExtensionAPI(
},
registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
runtime.assertActive();
extension.messageRenderers.set(customType, renderer as MessageRenderer);
},
// Flag access - checks extension registered it, reads from runtime
getFlag(name: string): boolean | string | undefined {
runtime.assertActive();
if (!extension.flags.has(name)) return undefined;
return runtime.flagValues.get(name);
},
// Action methods - delegate to shared runtime
sendMessage(message, options): void {
runtime.assertActive();
runtime.sendMessage(message, options);
},
sendUserMessage(content, options): void {
runtime.assertActive();
runtime.sendUserMessage(content, options);
},
appendEntry(customType: string, data?: unknown): void {
runtime.assertActive();
runtime.appendEntry(customType, data);
},
setSessionName(name: string): void {
runtime.assertActive();
runtime.setSessionName(name);
},
getSessionName(): string | undefined {
runtime.assertActive();
return runtime.getSessionName();
},
setLabel(entryId: string, label: string | undefined): void {
runtime.assertActive();
runtime.setLabel(entryId, label);
},
exec(command: string, args: string[], options?: ExecOptions) {
runtime.assertActive();
return execCommand(command, args, options?.cwd ?? cwd, options);
},
getActiveTools(): string[] {
runtime.assertActive();
return runtime.getActiveTools();
},
getAllTools() {
runtime.assertActive();
return runtime.getAllTools();
},
setActiveTools(toolNames: string[]): void {
runtime.assertActive();
runtime.setActiveTools(toolNames);
},
getCommands() {
runtime.assertActive();
return runtime.getCommands();
},
setModel(model) {
runtime.assertActive();
return runtime.setModel(model);
},
getThinkingLevel() {
runtime.assertActive();
return runtime.getThinkingLevel();
},
setThinkingLevel(level) {
runtime.assertActive();
runtime.setThinkingLevel(level);
},
registerProvider(name: string, config: ProviderConfig) {
runtime.assertActive();
runtime.registerProvider(name, config, extension.path);
},
unregisterProvider(name: string) {
runtime.assertActive();
runtime.unregisterProvider(name, extension.path);
},