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:
Helmut Januschka
2026-03-03 01:08:51 +01:00
committed by Mario Zechner
parent ade6a35e75
commit 7df89066d9
5 changed files with 129 additions and 11 deletions

View File

@@ -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(