From 7463803c950160715abf7a3f8b240b4b2ab91496 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 13 Aug 2026 08:51:05 +0000 Subject: [PATCH] fix(extensions): populated runtime mode in context Expose the Pi-compatible tui, rpc, json, or print host mode to every extension context and cover mode transitions in the runner regression suite. Fixes #8419 --- packages/coding-agent/CHANGELOG.md | 4 ++ .../src/extensibility/extensions/runner.ts | 5 ++ .../src/extensibility/extensions/types.ts | 5 ++ .../coding-agent/src/modes/acp/acp-agent.ts | 1 + .../controllers/extension-ui-controller.ts | 4 +- packages/coding-agent/src/modes/print-mode.ts | 1 + .../coding-agent/src/modes/rpc/rpc-mode.ts | 1 + .../coding-agent/src/modes/runtime-init.ts | 6 ++- .../coding-agent/src/session/agent-session.ts | 1 + .../test/extensions-runner.test.ts | 47 +++++++++++++++++++ 10 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 14334fddd..b40021406 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- 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 ### Breaking Changes diff --git a/packages/coding-agent/src/extensibility/extensions/runner.ts b/packages/coding-agent/src/extensibility/extensions/runner.ts index ad7e5417c..164cea650 100644 --- a/packages/coding-agent/src/extensibility/extensions/runner.ts +++ b/packages/coding-agent/src/extensibility/extensions/runner.ts @@ -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; #errorListeners: Set = 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(), diff --git a/packages/coding-agent/src/extensibility/extensions/types.ts b/packages/coding-agent/src/extensibility/extensions/types.ts index 766519f7a..7aaf590f8 100644 --- a/packages/coding-agent/src/extensibility/extensions/types.ts +++ b/packages/coding-agent/src/extensibility/extensions/types.ts @@ -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. */ diff --git a/packages/coding-agent/src/modes/acp/acp-agent.ts b/packages/coding-agent/src/modes/acp/acp-agent.ts index ef27632c5..ea3975f5d 100644 --- a/packages/coding-agent/src/modes/acp/acp-agent.ts +++ b/packages/coding-agent/src/modes/acp/acp-agent.ts @@ -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; diff --git a/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts b/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts index 1123ba668..c26a9310d 100644 --- a/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts +++ b/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts @@ -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"); } /** diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts index 612fb5408..c2a190456 100644 --- a/packages/coding-agent/src/modes/print-mode.ts +++ b/packages/coding-agent/src/modes/print-mode.ts @@ -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`, diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 67d7c1237..5ff4be444 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -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)); }, diff --git a/packages/coding-agent/src/modes/runtime-init.ts b/packages/coding-agent/src/modes/runtime-init.ts index 54ffe8635..d3ca0969b 100644 --- a/packages/coding-agent/src/modes/runtime-init.ts +++ b/packages/coding-agent/src/modes/runtime-init.ts @@ -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); diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 2a3f31bd0..f83ca3373 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -5647,6 +5647,7 @@ export class AgentSession { return { ui: noOpUIContext, + mode: "print", hasUI: false, cwd: this.sessionManager.getCwd(), sessionManager: this.sessionManager, diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index 059cf39c3..14d0f2d81 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -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 = `