feat(coding-agent): add session_directory extension event
Adds a session_directory extension event that fires before session manager creation, allowing extensions to customize the session directory path based on cwd, git branch, or other context. - Extensions can return a custom sessionDir in the event handler - CLI --session-dir flag takes precedence over extension-provided paths - If multiple extensions return a sessionDir, the last one wins - Enables implementing branch-based sessions as an extension instead of core feature This provides the extension point needed to implement git branch-based session directories without adding the complexity to core. Closes #1729
This commit is contained in:
committed by
Mario Zechner
parent
ade6a35e75
commit
7df89066d9
@@ -4,6 +4,7 @@
|
||||
|
||||
### Added
|
||||
- Added `/tree` branch folding and segment-jump navigation with `Ctrl+←`/`Ctrl+→` and `Alt+←`/`Alt+→`, while keeping `←`/`→` and `Page Up`/`Page Down` for paging ([#1724](https://github.com/badlogic/pi-mono/pull/1724) by [@Perlence](https://github.com/Perlence))
|
||||
- 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 ([#1730](https://github.com/badlogic/pi-mono/pull/1730) by [@hjanuschka](https://github.com/hjanuschka)).
|
||||
|
||||
### Fixed
|
||||
- Fixed custom tool collapsed/expanded rendering in HTML exports. Custom tools that define different collapsed vs expanded displays now render correctly in exported HTML, with expandable sections when both states differ and direct display when only expanded exists ([#1934](https://github.com/badlogic/pi-mono/pull/1934) by [@aliou](https://github.com/aliou))
|
||||
@@ -143,6 +144,10 @@
|
||||
- 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
|
||||
|
||||
|
||||
@@ -114,6 +114,8 @@ export type {
|
||||
SessionBeforeTreeEvent,
|
||||
SessionBeforeTreeResult,
|
||||
SessionCompactEvent,
|
||||
SessionDirectoryEvent,
|
||||
SessionDirectoryResult,
|
||||
SessionEvent,
|
||||
SessionForkEvent,
|
||||
SessionShutdownEvent,
|
||||
|
||||
@@ -851,6 +851,40 @@ 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<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. */
|
||||
async emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise<InputEventResult> {
|
||||
const ctx = this.createContext();
|
||||
|
||||
@@ -388,6 +388,14 @@ export interface ResourcesDiscoverResult {
|
||||
// Session Events
|
||||
// ============================================================================
|
||||
|
||||
/** Fired before session manager creation to allow custom session directory resolution */
|
||||
export interface SessionDirectoryEvent {
|
||||
type: "session_directory";
|
||||
cwd: string;
|
||||
/** CLI-provided session directory (if any) */
|
||||
cliSessionDir: string | undefined;
|
||||
}
|
||||
|
||||
/** Fired on initial session load */
|
||||
export interface SessionStartEvent {
|
||||
type: "session_start";
|
||||
@@ -472,6 +480,7 @@ export interface SessionTreeEvent {
|
||||
}
|
||||
|
||||
export type SessionEvent =
|
||||
| SessionDirectoryEvent
|
||||
| SessionStartEvent
|
||||
| SessionBeforeSwitchEvent
|
||||
| SessionSwitchEvent
|
||||
@@ -866,6 +875,11 @@ export interface BeforeAgentStartEventResult {
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
export interface SessionDirectoryResult {
|
||||
/** Custom session directory path. If multiple extensions return this, the last one wins. */
|
||||
sessionDir?: string;
|
||||
}
|
||||
|
||||
export interface SessionBeforeSwitchResult {
|
||||
cancel?: boolean;
|
||||
}
|
||||
@@ -936,6 +950,7 @@ export interface ExtensionAPI {
|
||||
// =========================================================================
|
||||
|
||||
on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
|
||||
on(event: "session_directory", handler: ExtensionHandler<SessionDirectoryEvent, SessionDirectoryResult>): void;
|
||||
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
||||
on(
|
||||
event: "session_before_switch",
|
||||
|
||||
@@ -379,17 +379,75 @@ async function promptConfirm(message: string): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
async function createSessionManager(parsed: Args, cwd: string): Promise<SessionManager | undefined> {
|
||||
/** Helper to call session_directory handlers from extensions before runner is fully initialized */
|
||||
async function callSessionDirectoryHook(
|
||||
extensions: LoadExtensionsResult,
|
||||
cwd: string,
|
||||
cliSessionDir: string | undefined,
|
||||
): Promise<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) {
|
||||
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;
|
||||
|
||||
if (result?.sessionDir) {
|
||||
customSessionDir = result.sessionDir;
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(chalk.red(`Extension "${ext.path}" session_directory handler failed: ${message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return customSessionDir;
|
||||
}
|
||||
|
||||
async function createSessionManager(
|
||||
parsed: Args,
|
||||
cwd: string,
|
||||
extensions: LoadExtensionsResult,
|
||||
): Promise<SessionManager | undefined> {
|
||||
if (parsed.noSession) {
|
||||
return SessionManager.inMemory();
|
||||
}
|
||||
|
||||
// CLI flag takes precedence, otherwise ask extensions for custom session directory
|
||||
let effectiveSessionDir = parsed.sessionDir;
|
||||
if (!effectiveSessionDir) {
|
||||
effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd, parsed.sessionDir);
|
||||
}
|
||||
|
||||
if (parsed.session) {
|
||||
const resolved = await resolveSessionPath(parsed.session, cwd, parsed.sessionDir);
|
||||
const resolved = await resolveSessionPath(parsed.session, cwd, effectiveSessionDir);
|
||||
|
||||
switch (resolved.type) {
|
||||
case "path":
|
||||
case "local":
|
||||
return SessionManager.open(resolved.path, parsed.sessionDir);
|
||||
return SessionManager.open(resolved.path, effectiveSessionDir);
|
||||
|
||||
case "global": {
|
||||
// Session found in different project - ask user if they want to fork
|
||||
@@ -399,7 +457,7 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
||||
console.log(chalk.dim("Aborted."));
|
||||
process.exit(0);
|
||||
}
|
||||
return SessionManager.forkFrom(resolved.path, cwd, parsed.sessionDir);
|
||||
return SessionManager.forkFrom(resolved.path, cwd, effectiveSessionDir);
|
||||
}
|
||||
|
||||
case "not_found":
|
||||
@@ -408,12 +466,12 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
||||
}
|
||||
}
|
||||
if (parsed.continue) {
|
||||
return SessionManager.continueRecent(cwd, parsed.sessionDir);
|
||||
return SessionManager.continueRecent(cwd, effectiveSessionDir);
|
||||
}
|
||||
// --resume is handled separately (needs picker UI)
|
||||
// If --session-dir provided without --continue/--resume, create new session there
|
||||
if (parsed.sessionDir) {
|
||||
return SessionManager.create(cwd, parsed.sessionDir);
|
||||
// If effective session dir is set, create new session there
|
||||
if (effectiveSessionDir) {
|
||||
return SessionManager.create(cwd, effectiveSessionDir);
|
||||
}
|
||||
// Default case (new session) returns undefined, SDK will create one
|
||||
return undefined;
|
||||
@@ -676,15 +734,19 @@ export async function main(args: string[]) {
|
||||
}
|
||||
|
||||
// Create session manager based on CLI flags
|
||||
let sessionManager = await createSessionManager(parsed, cwd);
|
||||
let sessionManager = await createSessionManager(parsed, cwd, extensionsResult);
|
||||
|
||||
// Handle --resume: show session picker
|
||||
if (parsed.resume) {
|
||||
// Initialize keybindings so session picker respects user config
|
||||
KeybindingsManager.create();
|
||||
|
||||
// Compute effective session dir for resume (same logic as createSessionManager)
|
||||
const effectiveSessionDir =
|
||||
parsed.sessionDir || (await callSessionDirectoryHook(extensionsResult, cwd, parsed.sessionDir));
|
||||
|
||||
const selectedPath = await selectSession(
|
||||
(onProgress) => SessionManager.list(cwd, parsed.sessionDir, onProgress),
|
||||
(onProgress) => SessionManager.list(cwd, effectiveSessionDir, onProgress),
|
||||
SessionManager.listAll,
|
||||
);
|
||||
if (!selectedPath) {
|
||||
@@ -692,7 +754,7 @@ export async function main(args: string[]) {
|
||||
stopThemeWatcher();
|
||||
process.exit(0);
|
||||
}
|
||||
sessionManager = SessionManager.open(selectedPath);
|
||||
sessionManager = SessionManager.open(selectedPath, effectiveSessionDir);
|
||||
}
|
||||
|
||||
const { options: sessionOptions, cliThinkingFromModel } = buildSessionOptions(
|
||||
|
||||
Reference in New Issue
Block a user