diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 41b708708..67ec12711 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -144,11 +144,6 @@ - Fixed Bedrock `AWS_PROFILE` region resolution by honoring profile `region` values ([#1800](https://github.com/badlogic/pi-mono/issues/1800)). - Fixed Gemini 3.1 thinking-level detection for `google` and `google-vertex` providers ([#1785](https://github.com/badlogic/pi-mono/issues/1785)). - Fixed browser bundling compatibility for `@mariozechner/pi-ai` by removing Node-only side effects from default browser import paths ([#1814](https://github.com/badlogic/pi-mono/issues/1814)). -======= - -- Added `session_directory` extension event that fires before session manager creation, allowing extensions to customize the session directory path based on cwd and other factors. CLI `--session-dir` flag takes precedence over extension-provided paths ([#1729](https://github.com/badlogic/pi-mono/issues/1729)). ->>>>>>> ddf3c31b (feat(coding-agent): add session_directory extension event) - ## [0.55.4] - 2026-03-02 ### New Features diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 66282de2e..10d3ab79b 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -225,8 +225,9 @@ Run `npm install` in the extension directory, then imports from `node_modules/` ### Lifecycle Overview ``` -pi starts +pi starts (CLI only) │ + ├─► session_directory (CLI startup only, no ctx) └─► session_start │ ▼ @@ -285,6 +286,26 @@ exit (Ctrl+C, Ctrl+D) See [session.md](session.md) for session storage internals and the SessionManager API. +#### session_directory + +Fired by the `pi` CLI during startup session resolution, before the initial session manager is created. + +This event is: +- CLI-only. It is not emitted in SDK mode. +- Startup-only. It is not emitted for later interactive `/new` or `/resume` actions. +- Bypassed when `--session-dir` is provided. +- Special-cased to receive no `ctx` argument. + +If multiple extensions return `sessionDir`, the last one wins. + +```typescript +pi.on("session_directory", async (event) => { + return { + sessionDir: `/tmp/pi-sessions/${encodeURIComponent(event.cwd)}`, + }; +}); +``` + #### session_start Fired on initial session load. @@ -674,7 +695,9 @@ Transforms chain across handlers. See [input-transform.ts](../examples/extension ## ExtensionContext -Every handler receives `ctx: ExtensionContext`: +All handlers except `session_directory` receive `ctx: ExtensionContext`. + +`session_directory` is a CLI startup hook and receives only the event. ### ctx.ui diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 2d0fce720..39b4a66e4 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -115,6 +115,7 @@ export type { SessionBeforeTreeResult, SessionCompactEvent, SessionDirectoryEvent, + SessionDirectoryHandler, SessionDirectoryResult, SessionEvent, SessionForkEvent, diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index 3c62134ae..05f2d786f 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -851,40 +851,6 @@ export class ExtensionRunner { return { skillPaths, promptPaths, themePaths }; } - /** Emit session_directory event. Returns custom session directory from extensions (last one wins). */ - async emitSessionDirectory(cwd: string, cliSessionDir: string | undefined): Promise { - const ctx = this.createContext(); - let customSessionDir: string | undefined; - - for (const ext of this.extensions) { - const handlers = ext.handlers.get("session_directory"); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const event = { type: "session_directory" as const, cwd, cliSessionDir }; - const handlerResult = await handler(event, ctx); - const result = handlerResult as { sessionDir?: string } | undefined; - - if (result?.sessionDir) { - customSessionDir = result.sessionDir; - } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const stack = err instanceof Error ? err.stack : undefined; - this.emitError({ - extensionPath: ext.path, - event: "session_directory", - error: message, - stack, - }); - } - } - } - - return customSessionDir; - } - /** Emit input event. Transforms chain, "handled" short-circuits. */ async emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise { const ctx = this.createContext(); diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index f30a867c3..69ddb323f 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -392,8 +392,6 @@ export interface ResourcesDiscoverResult { export interface SessionDirectoryEvent { type: "session_directory"; cwd: string; - /** CLI-provided session directory (if any) */ - cliSessionDir: string | undefined; } /** Fired on initial session load */ @@ -880,6 +878,11 @@ export interface SessionDirectoryResult { sessionDir?: string; } +/** Special startup-only handler. Unlike other events, this receives no ExtensionContext. */ +export type SessionDirectoryHandler = ( + event: SessionDirectoryEvent, +) => Promise | SessionDirectoryResult | undefined; + export interface SessionBeforeSwitchResult { cancel?: boolean; } @@ -950,7 +953,7 @@ export interface ExtensionAPI { // ========================================================================= on(event: "resources_discover", handler: ExtensionHandler): void; - on(event: "session_directory", handler: ExtensionHandler): void; + on(event: "session_directory", handler: SessionDirectoryHandler): void; on(event: "session_start", handler: ExtensionHandler): void; on( event: "session_before_switch", diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index edc43088d..6bb3d9a1e 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -379,39 +379,18 @@ async function promptConfirm(message: string): Promise { }); } -/** Helper to call session_directory handlers from extensions before runner is fully initialized */ -async function callSessionDirectoryHook( - extensions: LoadExtensionsResult, - cwd: string, - cliSessionDir: string | undefined, -): Promise { +/** Helper to call CLI-only session_directory handlers before the initial session manager is created */ +async function callSessionDirectoryHook(extensions: LoadExtensionsResult, cwd: string): Promise { let customSessionDir: string | undefined; - // Minimal context for this early event - most context actions will throw if called - const ctx = { - ui: { notify: () => {}, setStatus: () => {}, setWorkingMessage: () => {} } as any, - hasUI: false, - cwd, - sessionManager: undefined as any, - modelRegistry: undefined as any, - model: undefined, - isIdle: () => true, - abort: () => {}, - hasPendingMessages: () => false, - shutdown: () => process.exit(0), - getContextUsage: () => undefined, - compact: () => {}, - getSystemPrompt: () => "", - }; - for (const ext of extensions.extensions) { const handlers = ext.handlers.get("session_directory"); if (!handlers || handlers.length === 0) continue; for (const handler of handlers) { try { - const event = { type: "session_directory" as const, cwd, cliSessionDir }; - const result = (await handler(event, ctx)) as { sessionDir?: string } | undefined; + const event = { type: "session_directory" as const, cwd }; + const result = (await handler(event)) as { sessionDir?: string } | undefined; if (result?.sessionDir) { customSessionDir = result.sessionDir; @@ -438,7 +417,7 @@ async function createSessionManager( // CLI flag takes precedence, otherwise ask extensions for custom session directory let effectiveSessionDir = parsed.sessionDir; if (!effectiveSessionDir) { - effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd, parsed.sessionDir); + effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd); } if (parsed.session) { @@ -742,8 +721,7 @@ export async function main(args: string[]) { KeybindingsManager.create(); // Compute effective session dir for resume (same logic as createSessionManager) - const effectiveSessionDir = - parsed.sessionDir || (await callSessionDirectoryHook(extensionsResult, cwd, parsed.sessionDir)); + const effectiveSessionDir = parsed.sessionDir || (await callSessionDirectoryHook(extensionsResult, cwd)); const selectedPath = await selectSession( (onProgress) => SessionManager.list(cwd, effectiveSessionDir, onProgress),