feat(coding-agent): refine session_directory hook closes #1729
This commit is contained in:
@@ -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 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 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)).
|
- 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
|
## [0.55.4] - 2026-03-02
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|||||||
@@ -225,8 +225,9 @@ Run `npm install` in the extension directory, then imports from `node_modules/`
|
|||||||
### Lifecycle Overview
|
### Lifecycle Overview
|
||||||
|
|
||||||
```
|
```
|
||||||
pi starts
|
pi starts (CLI only)
|
||||||
│
|
│
|
||||||
|
├─► session_directory (CLI startup only, no ctx)
|
||||||
└─► session_start
|
└─► session_start
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -285,6 +286,26 @@ exit (Ctrl+C, Ctrl+D)
|
|||||||
|
|
||||||
See [session.md](session.md) for session storage internals and the SessionManager API.
|
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
|
#### session_start
|
||||||
|
|
||||||
Fired on initial session load.
|
Fired on initial session load.
|
||||||
@@ -674,7 +695,9 @@ Transforms chain across handlers. See [input-transform.ts](../examples/extension
|
|||||||
|
|
||||||
## ExtensionContext
|
## 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
|
### ctx.ui
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export type {
|
|||||||
SessionBeforeTreeResult,
|
SessionBeforeTreeResult,
|
||||||
SessionCompactEvent,
|
SessionCompactEvent,
|
||||||
SessionDirectoryEvent,
|
SessionDirectoryEvent,
|
||||||
|
SessionDirectoryHandler,
|
||||||
SessionDirectoryResult,
|
SessionDirectoryResult,
|
||||||
SessionEvent,
|
SessionEvent,
|
||||||
SessionForkEvent,
|
SessionForkEvent,
|
||||||
|
|||||||
@@ -851,40 +851,6 @@ export class ExtensionRunner {
|
|||||||
return { skillPaths, promptPaths, themePaths };
|
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<string | undefined> {
|
|
||||||
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. */
|
/** Emit input event. Transforms chain, "handled" short-circuits. */
|
||||||
async emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise<InputEventResult> {
|
async emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise<InputEventResult> {
|
||||||
const ctx = this.createContext();
|
const ctx = this.createContext();
|
||||||
|
|||||||
@@ -392,8 +392,6 @@ export interface ResourcesDiscoverResult {
|
|||||||
export interface SessionDirectoryEvent {
|
export interface SessionDirectoryEvent {
|
||||||
type: "session_directory";
|
type: "session_directory";
|
||||||
cwd: string;
|
cwd: string;
|
||||||
/** CLI-provided session directory (if any) */
|
|
||||||
cliSessionDir: string | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fired on initial session load */
|
/** Fired on initial session load */
|
||||||
@@ -880,6 +878,11 @@ export interface SessionDirectoryResult {
|
|||||||
sessionDir?: string;
|
sessionDir?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Special startup-only handler. Unlike other events, this receives no ExtensionContext. */
|
||||||
|
export type SessionDirectoryHandler = (
|
||||||
|
event: SessionDirectoryEvent,
|
||||||
|
) => Promise<SessionDirectoryResult | undefined> | SessionDirectoryResult | undefined;
|
||||||
|
|
||||||
export interface SessionBeforeSwitchResult {
|
export interface SessionBeforeSwitchResult {
|
||||||
cancel?: boolean;
|
cancel?: boolean;
|
||||||
}
|
}
|
||||||
@@ -950,7 +953,7 @@ export interface ExtensionAPI {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
|
on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
|
||||||
on(event: "session_directory", handler: ExtensionHandler<SessionDirectoryEvent, SessionDirectoryResult>): void;
|
on(event: "session_directory", handler: SessionDirectoryHandler): void;
|
||||||
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
||||||
on(
|
on(
|
||||||
event: "session_before_switch",
|
event: "session_before_switch",
|
||||||
|
|||||||
@@ -379,39 +379,18 @@ async function promptConfirm(message: string): Promise<boolean> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper to call session_directory handlers from extensions before runner is fully initialized */
|
/** Helper to call CLI-only session_directory handlers before the initial session manager is created */
|
||||||
async function callSessionDirectoryHook(
|
async function callSessionDirectoryHook(extensions: LoadExtensionsResult, cwd: string): Promise<string | undefined> {
|
||||||
extensions: LoadExtensionsResult,
|
|
||||||
cwd: string,
|
|
||||||
cliSessionDir: string | undefined,
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
let customSessionDir: string | undefined;
|
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) {
|
for (const ext of extensions.extensions) {
|
||||||
const handlers = ext.handlers.get("session_directory");
|
const handlers = ext.handlers.get("session_directory");
|
||||||
if (!handlers || handlers.length === 0) continue;
|
if (!handlers || handlers.length === 0) continue;
|
||||||
|
|
||||||
for (const handler of handlers) {
|
for (const handler of handlers) {
|
||||||
try {
|
try {
|
||||||
const event = { type: "session_directory" as const, cwd, cliSessionDir };
|
const event = { type: "session_directory" as const, cwd };
|
||||||
const result = (await handler(event, ctx)) as { sessionDir?: string } | undefined;
|
const result = (await handler(event)) as { sessionDir?: string } | undefined;
|
||||||
|
|
||||||
if (result?.sessionDir) {
|
if (result?.sessionDir) {
|
||||||
customSessionDir = result.sessionDir;
|
customSessionDir = result.sessionDir;
|
||||||
@@ -438,7 +417,7 @@ async function createSessionManager(
|
|||||||
// CLI flag takes precedence, otherwise ask extensions for custom session directory
|
// CLI flag takes precedence, otherwise ask extensions for custom session directory
|
||||||
let effectiveSessionDir = parsed.sessionDir;
|
let effectiveSessionDir = parsed.sessionDir;
|
||||||
if (!effectiveSessionDir) {
|
if (!effectiveSessionDir) {
|
||||||
effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd, parsed.sessionDir);
|
effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.session) {
|
if (parsed.session) {
|
||||||
@@ -742,8 +721,7 @@ export async function main(args: string[]) {
|
|||||||
KeybindingsManager.create();
|
KeybindingsManager.create();
|
||||||
|
|
||||||
// Compute effective session dir for resume (same logic as createSessionManager)
|
// Compute effective session dir for resume (same logic as createSessionManager)
|
||||||
const effectiveSessionDir =
|
const effectiveSessionDir = parsed.sessionDir || (await callSessionDirectoryHook(extensionsResult, cwd));
|
||||||
parsed.sessionDir || (await callSessionDirectoryHook(extensionsResult, cwd, parsed.sessionDir));
|
|
||||||
|
|
||||||
const selectedPath = await selectSession(
|
const selectedPath = await selectSession(
|
||||||
(onProgress) => SessionManager.list(cwd, effectiveSessionDir, onProgress),
|
(onProgress) => SessionManager.list(cwd, effectiveSessionDir, onProgress),
|
||||||
|
|||||||
Reference in New Issue
Block a user