From 502067c213b0a4c29c74098852418fd376752701 Mon Sep 17 00:00:00 2001 From: frizynn Date: Thu, 19 Mar 2026 16:38:51 -0300 Subject: [PATCH] refactor: deduplicate RPC mode shared patterns Extract the duplicated commandContextActions implementations from rpc-mode.ts and print-mode.ts into a shared createDefaultCommandContextActions() factory in modes/shared/command-context-actions.ts. Both headless modes (RPC and print) had identical implementations of waitForIdle, newSession, fork, navigateTree, switchSession, and reload that simply delegate to AgentSession. The shared factory provides these defaults; interactive mode continues to layer TUI-specific behavior on top via its own overrides. Also fixes a subtle redundancy in print mode where newSession manually called options.setup after session.newSession(), even though session.newSession() already handles the setup callback internally. --- .../pi-coding-agent/src/modes/print-mode.ts | 32 +---------- .../pi-coding-agent/src/modes/rpc/rpc-mode.ts | 30 +---------- .../modes/shared/command-context-actions.ts | 53 +++++++++++++++++++ 3 files changed, 57 insertions(+), 58 deletions(-) create mode 100644 packages/pi-coding-agent/src/modes/shared/command-context-actions.ts diff --git a/packages/pi-coding-agent/src/modes/print-mode.ts b/packages/pi-coding-agent/src/modes/print-mode.ts index 479bae1fd..a2557f99b 100644 --- a/packages/pi-coding-agent/src/modes/print-mode.ts +++ b/packages/pi-coding-agent/src/modes/print-mode.ts @@ -8,6 +8,7 @@ import type { AssistantMessage, ImageContent } from "@gsd/pi-ai"; import type { AgentSession } from "../core/agent-session.js"; +import { createDefaultCommandContextActions } from "./shared/command-context-actions.js"; /** * Options for print mode. @@ -37,36 +38,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti } // Set up extensions for print mode (no UI) await session.bindExtensions({ - commandContextActions: { - waitForIdle: () => session.agent.waitForIdle(), - newSession: async (options) => { - const success = await session.newSession({ parentSession: options?.parentSession }); - if (success && options?.setup) { - await options.setup(session.sessionManager); - } - return { cancelled: !success }; - }, - fork: async (entryId) => { - const result = await session.fork(entryId); - return { cancelled: result.cancelled }; - }, - navigateTree: async (targetId, options) => { - const result = await session.navigateTree(targetId, { - summarize: options?.summarize, - customInstructions: options?.customInstructions, - replaceInstructions: options?.replaceInstructions, - label: options?.label, - }); - return { cancelled: result.cancelled }; - }, - switchSession: async (sessionPath) => { - const success = await session.switchSession(sessionPath); - return { cancelled: !success }; - }, - reload: async () => { - await session.reload(); - }, - }, + commandContextActions: createDefaultCommandContextActions(session), onError: (err) => { console.error(`Extension error (${err.extensionPath}): ${err.error}`); }, diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts index dc02b4491..5d076fcd5 100644 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts @@ -19,6 +19,7 @@ import type { ExtensionWidgetOptions, } from "../../core/extensions/index.js"; import { type Theme, theme } from "../interactive/theme/theme.js"; +import { createDefaultCommandContextActions } from "../shared/command-context-actions.js"; import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js"; import type { RpcCommand, @@ -285,34 +286,7 @@ export async function runRpcMode(session: AgentSession): Promise { // Set up extensions with RPC-based UI context await session.bindExtensions({ uiContext: createExtensionUIContext(), - commandContextActions: { - waitForIdle: () => session.agent.waitForIdle(), - newSession: async (options) => { - // Delegate to AgentSession (handles setup + agent state sync) - const success = await session.newSession(options); - return { cancelled: !success }; - }, - fork: async (entryId) => { - const result = await session.fork(entryId); - return { cancelled: result.cancelled }; - }, - navigateTree: async (targetId, options) => { - const result = await session.navigateTree(targetId, { - summarize: options?.summarize, - customInstructions: options?.customInstructions, - replaceInstructions: options?.replaceInstructions, - label: options?.label, - }); - return { cancelled: result.cancelled }; - }, - switchSession: async (sessionPath) => { - const success = await session.switchSession(sessionPath); - return { cancelled: !success }; - }, - reload: async () => { - await session.reload(); - }, - }, + commandContextActions: createDefaultCommandContextActions(session), shutdownHandler: () => { shutdownRequested = true; }, diff --git a/packages/pi-coding-agent/src/modes/shared/command-context-actions.ts b/packages/pi-coding-agent/src/modes/shared/command-context-actions.ts new file mode 100644 index 000000000..05d268879 --- /dev/null +++ b/packages/pi-coding-agent/src/modes/shared/command-context-actions.ts @@ -0,0 +1,53 @@ +/** + * Default (headless) implementations of ExtensionCommandContextActions. + * + * These delegate directly to AgentSession without any UI side-effects. + * Interactive mode layers TUI-specific behavior on top of these. + * RPC and print modes use them as-is. + */ + +import type { AgentSession } from "../../core/agent-session.js"; +import type { ExtensionCommandContextActions } from "../../core/extensions/index.js"; + +/** + * Create the default set of command context actions that simply delegate + * to the corresponding AgentSession methods. + * + * Callers can spread the result and override individual actions to add + * mode-specific behavior (e.g., interactive mode clears TUI state after + * forking). + */ +export function createDefaultCommandContextActions(session: AgentSession): ExtensionCommandContextActions { + return { + waitForIdle: () => session.agent.waitForIdle(), + + newSession: async (options) => { + const success = await session.newSession(options); + return { cancelled: !success }; + }, + + fork: async (entryId) => { + const result = await session.fork(entryId); + return { cancelled: result.cancelled }; + }, + + navigateTree: async (targetId, options) => { + const result = await session.navigateTree(targetId, { + summarize: options?.summarize, + customInstructions: options?.customInstructions, + replaceInstructions: options?.replaceInstructions, + label: options?.label, + }); + return { cancelled: result.cancelled }; + }, + + switchSession: async (sessionPath) => { + const success = await session.switchSession(sessionPath); + return { cancelled: !success }; + }, + + reload: async () => { + await session.reload(); + }, + }; +}