Fix branch selector for single message and --no-session mode

- Allow branch selector to open with single user message (changed <= 1 to === 0 check)
- Support in-memory branching for --no-session mode (no files created)
- Add isEnabled() getter to SessionManager
- Update sessionFile getter to return null when sessions disabled
- Update SessionSwitchEvent types to allow null session files
- Add branching tests for single message and --no-session scenarios

fixes #163
This commit is contained in:
Mario Zechner 2025-12-10 22:41:32 +01:00
parent 09a48fd1c3
commit 3d35e7c469
10 changed files with 292 additions and 27 deletions

View file

@ -76,7 +76,7 @@ export interface CompactionResult {
/** Session statistics for /session command */
export interface SessionStats {
sessionFile: string;
sessionFile: string | null;
sessionId: string;
userMessages: number;
assistantMessages: number;
@ -320,9 +320,9 @@ export class AgentSession {
return this.agent.getQueueMode();
}
/** Current session file path */
get sessionFile(): string {
return this.sessionManager.getSessionFile();
/** Current session file path, or null if sessions are disabled */
get sessionFile(): string | null {
return this.sessionManager.isEnabled() ? this.sessionManager.getSessionFile() : null;
}
/** Current session ID */
@ -966,11 +966,15 @@ export class AgentSession {
return { selectedText, skipped: true };
}
// Create branched session
// Create branched session (returns null in --no-session mode)
const newSessionFile = this.sessionManager.createBranchedSessionFromEntries(entries, entryIndex);
this.sessionManager.setSessionFile(newSessionFile);
// Emit session_switch event
// Update session file if we have one (file-based mode)
if (newSessionFile !== null) {
this.sessionManager.setSessionFile(newSessionFile);
}
// Emit session_switch event (in --no-session mode, both files are null)
if (this._hookRunner) {
this._hookRunner.setSessionFile(newSessionFile);
await this._hookRunner.emit({
@ -981,7 +985,7 @@ export class AgentSession {
});
}
// Reload
// Reload messages from entries (works for both file and in-memory mode)
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
this.agent.replaceMessages(loaded.messages);

View file

@ -86,10 +86,10 @@ export interface SessionStartEvent {
*/
export interface SessionSwitchEvent {
type: "session_switch";
/** New session file path */
newSessionFile: string;
/** Previous session file path */
previousSessionFile: string;
/** New session file path, or null in --no-session mode */
newSessionFile: string | null;
/** Previous session file path, or null in --no-session mode */
previousSessionFile: string | null;
/** Reason for the switch */
reason: "branch" | "switch";
}

View file

@ -231,6 +231,11 @@ export class SessionManager {
this.enabled = false;
}
/** Check if session persistence is enabled */
isEnabled(): boolean {
return this.enabled;
}
private getSessionDirectory(): string {
const cwd = process.cwd();
// Replace all path separators and colons (for Windows drive letters) with dashes
@ -637,32 +642,43 @@ export class SessionManager {
/**
* Create a branched session from session entries up to (but not including) a specific entry index.
* This preserves compaction events and all entry types.
* Returns the new session file path.
* Returns the new session file path, or null if in --no-session mode (in-memory only).
*/
createBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string {
createBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null {
const newSessionId = uuidv4();
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);
// Copy all entries up to (but not including) the branch point
// Build new entries list (up to but not including branch point)
const newEntries: SessionEntry[] = [];
for (let i = 0; i < branchBeforeIndex; i++) {
const entry = entries[i];
if (entry.type === "session") {
// Rewrite session header with new ID and branchedFrom
const newHeader: SessionHeader = {
newEntries.push({
...entry,
id: newSessionId,
timestamp: new Date().toISOString(),
branchedFrom: this.sessionFile,
};
appendFileSync(newSessionFile, JSON.stringify(newHeader) + "\n");
branchedFrom: this.enabled ? this.sessionFile : undefined,
});
} else {
// Copy other entries as-is
appendFileSync(newSessionFile, JSON.stringify(entry) + "\n");
newEntries.push(entry);
}
}
return newSessionFile;
if (this.enabled) {
// Write to file
for (const entry of newEntries) {
appendFileSync(newSessionFile, JSON.stringify(entry) + "\n");
}
return newSessionFile;
} else {
// In-memory mode: replace inMemoryEntries, no file created
this.inMemoryEntries = newEntries;
this.sessionId = newSessionId;
return null;
}
}
}