Merge PR #8423: fix(extensions): populate runtime mode in context (@roboomp)
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed the status-line git branch display freezing on the previous branch after the first branch switch, caused by the HEAD watcher binding to a file inode that git unlinks on its atomic HEAD rename ([#8412](https://github.com/can1357/oh-my-pi/issues/8412)).
|
||||
- Fixed Pi extension contexts omitting the runtime `mode`, which made documented TUI guards silently disable extension UI ([#8419](https://github.com/can1357/oh-my-pi/issues/8419)).
|
||||
|
||||
## [17.3.0] - 2026-08-13
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import type {
|
||||
ExtensionError,
|
||||
ExtensionEvent,
|
||||
ExtensionFlag,
|
||||
ExtensionMode,
|
||||
ExtensionRuntime,
|
||||
ExtensionShortcut,
|
||||
ExtensionUIContext,
|
||||
@@ -342,6 +343,7 @@ interface ToolRegistrationScope {
|
||||
|
||||
export class ExtensionRunner {
|
||||
#uiContext: ExtensionUIContext;
|
||||
#mode: ExtensionMode = "print";
|
||||
#toolApprovalPreviewWaiter?: (toolCallId: string) => Promise<void>;
|
||||
#errorListeners: Set<ExtensionErrorListener> = new Set();
|
||||
#getModel: () => Model | undefined = () => undefined;
|
||||
@@ -525,6 +527,7 @@ export class ExtensionRunner {
|
||||
contextActions: ExtensionContextActions,
|
||||
commandContextActions?: ExtensionCommandContextActions,
|
||||
uiContext?: ExtensionUIContext,
|
||||
mode: ExtensionMode = "print",
|
||||
): void {
|
||||
// Copy actions into the shared runtime (all extension APIs reference this)
|
||||
this.runtime.sendMessage = actions.sendMessage;
|
||||
@@ -573,6 +576,7 @@ export class ExtensionRunner {
|
||||
}
|
||||
|
||||
this.#uiContext = uiContext ?? noOpUIContext;
|
||||
this.#mode = mode;
|
||||
this.#initialized = true;
|
||||
|
||||
// Drain events buffered by emitCredentialDisabled() before initialize ran. The
|
||||
@@ -953,6 +957,7 @@ export class ExtensionRunner {
|
||||
const getModel = model ? () => model : this.#getModel;
|
||||
return {
|
||||
ui: this.#uiContext,
|
||||
mode: this.#mode,
|
||||
getContextUsage: () => this.#getContextUsageFn(),
|
||||
compact: instructionsOrOptions => this.#compactFn(instructionsOrOptions),
|
||||
getAsyncJobSnapshot: () => this.#getAsyncJobSnapshotFn(),
|
||||
|
||||
@@ -434,9 +434,14 @@ export interface ExtensionModelQuery {
|
||||
family(model: Model): string;
|
||||
}
|
||||
|
||||
/** Runtime host mode exposed to Pi-compatible extensions. */
|
||||
export type ExtensionMode = "tui" | "rpc" | "json" | "print";
|
||||
|
||||
export interface ExtensionContext {
|
||||
/** UI methods for user interaction */
|
||||
ui: ExtensionUIContext;
|
||||
/** Current run mode. Use `"tui"` to guard terminal-only UI such as custom components. */
|
||||
mode: ExtensionMode;
|
||||
/** Get current context usage for the active model. */
|
||||
getContextUsage(): ContextUsage | undefined;
|
||||
/** Get a read-only snapshot of async jobs owned by this session. */
|
||||
|
||||
@@ -2419,6 +2419,7 @@ export class AcpAgent implements Agent {
|
||||
compact: instructionsOrOptions => runExtensionCompact(record.session, instructionsOrOptions),
|
||||
},
|
||||
uiContext,
|
||||
"rpc",
|
||||
);
|
||||
await extensionRunner.emit({ type: "session_start" });
|
||||
record.extensionsConfigured = true;
|
||||
|
||||
@@ -283,7 +283,7 @@ export class ExtensionUiController {
|
||||
},
|
||||
};
|
||||
|
||||
extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
|
||||
extensionRunner.initialize(actions, contextActions, commandActions, uiContext, "tui");
|
||||
|
||||
// Subscribe to extension errors
|
||||
extensionRunner.onError((error: ExtensionError) => {
|
||||
@@ -512,7 +512,7 @@ export class ExtensionUiController {
|
||||
},
|
||||
};
|
||||
|
||||
extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
|
||||
extensionRunner.initialize(actions, contextActions, commandActions, uiContext, "tui");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,6 +118,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
||||
}
|
||||
// Set up extensions for print mode (no UI, no command context)
|
||||
await initializeExtensions(session, {
|
||||
mode: mode === "json" ? "json" : "print",
|
||||
reportSendError: (action, err) => {
|
||||
process.stderr.write(
|
||||
`Extension ${action === "extension_send" ? "sendMessage" : "sendUserMessage"} failed: ${err.message}\n`,
|
||||
|
||||
@@ -933,6 +933,7 @@ export async function runRpcMode(
|
||||
|
||||
// Set up extensions with RPC-based UI context
|
||||
await initializeExtensions(session, {
|
||||
mode: "rpc",
|
||||
reportSendError: (action, err) => {
|
||||
output(error(undefined, action, err.message));
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
||||
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
||||
import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
|
||||
import type { ExtensionError, ExtensionMode, ExtensionUIContext } from "../extensibility/extensions/types";
|
||||
import type { AgentSession } from "../session/agent-session";
|
||||
import { USER_INTERRUPT_LABEL } from "../session/messages";
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface InitializeExtensionsOptions {
|
||||
reportRuntimeError: (error: ExtensionError) => void;
|
||||
/** Optional shutdown hook (rpc mode signals its loop; print mode is a no-op). */
|
||||
onShutdown?: () => void;
|
||||
/** Pi-compatible mode exposed to extension contexts. Defaults to `"print"`. */
|
||||
mode?: ExtensionMode;
|
||||
/** Optional UI context (rpc supplies one; print runs headless). */
|
||||
uiContext?: ExtensionUIContext;
|
||||
/** Optional lifecycle hook for extension-originated messages that can start an agent turn. */
|
||||
@@ -44,6 +46,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
||||
reportSendError,
|
||||
reportRuntimeError,
|
||||
onShutdown,
|
||||
mode = "print",
|
||||
uiContext,
|
||||
markAgentInvokingMessage,
|
||||
trackAgentInvokingMessage,
|
||||
@@ -137,6 +140,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
||||
compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
|
||||
},
|
||||
uiContext,
|
||||
mode,
|
||||
);
|
||||
|
||||
runner.onError(reportRuntimeError);
|
||||
|
||||
@@ -5647,6 +5647,7 @@ export class AgentSession {
|
||||
|
||||
return {
|
||||
ui: noOpUIContext,
|
||||
mode: "print",
|
||||
hasUI: false,
|
||||
cwd: this.sessionManager.getCwd(),
|
||||
sessionManager: this.sessionManager,
|
||||
|
||||
@@ -104,6 +104,53 @@ describe("ExtensionRunner", () => {
|
||||
expect(runner.createContext().cwd).toBe(dirB);
|
||||
});
|
||||
|
||||
it("exposes the initialized host mode to extension contexts", async () => {
|
||||
const result = await loadTestExtensions();
|
||||
const runner = new ExtensionRunner(
|
||||
result.extensions,
|
||||
result.runtime,
|
||||
tempDir.path(),
|
||||
sessionManager,
|
||||
modelRegistry,
|
||||
);
|
||||
const actions = {
|
||||
sendMessage: () => {},
|
||||
sendUserMessage: () => {},
|
||||
appendEntry: () => {},
|
||||
setLabel: () => {},
|
||||
getActiveTools: () => [],
|
||||
getAllTools: () => [],
|
||||
setActiveTools: async () => {},
|
||||
getCommands: () => [],
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => undefined,
|
||||
setThinkingLevel: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setSessionName: async () => {},
|
||||
};
|
||||
const contextActions = {
|
||||
getModel: () => undefined,
|
||||
isIdle: () => true,
|
||||
abort: () => {},
|
||||
hasPendingMessages: () => false,
|
||||
shutdown: () => {},
|
||||
getContextUsage: () => undefined,
|
||||
compact: async () => {},
|
||||
getSystemPrompt: () => [],
|
||||
};
|
||||
|
||||
expect(runner.createContext().mode).toBe("print");
|
||||
|
||||
runner.initialize(actions, contextActions, undefined, undefined, "rpc");
|
||||
expect(runner.createContext().mode).toBe("rpc");
|
||||
|
||||
runner.initialize(actions, contextActions, undefined, undefined, "json");
|
||||
expect(runner.createContext().mode).toBe("json");
|
||||
|
||||
runner.initialize(actions, contextActions, undefined, undefined, "tui");
|
||||
expect(runner.createContext().mode).toBe("tui");
|
||||
});
|
||||
|
||||
describe("shortcut conflicts", () => {
|
||||
it("warns when extension shortcut conflicts with built-in", async () => {
|
||||
const extCode = `
|
||||
|
||||
Reference in New Issue
Block a user