mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 14:03:49 +00:00
Refactor SessionManager to use static factory methods
- Add factory methods: create(cwd), open(path), continueRecent(cwd), inMemory() - Add static list(cwd) for session listing - Make constructor private, pass cwd explicitly - Update SDK to take sessionManager instead of sessionFile options - Update main.ts to create SessionManager based on CLI flags - Update SessionSelectorComponent to take sessions[] instead of SessionManager - Update tests to use factory methods
This commit is contained in:
parent
7bf4c8ff24
commit
ace8ea3d5b
9 changed files with 346 additions and 473 deletions
|
|
@ -3,17 +3,17 @@
|
|||
*/
|
||||
|
||||
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
||||
import type { SessionManager } from "../core/session-manager.js";
|
||||
import type { SessionInfo } from "../core/session-manager.js";
|
||||
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js";
|
||||
|
||||
/** Show TUI session selector and return selected session path or null if cancelled */
|
||||
export async function selectSession(sessionManager: SessionManager): Promise<string | null> {
|
||||
export async function selectSession(sessions: SessionInfo[]): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const ui = new TUI(new ProcessTerminal());
|
||||
let resolved = false;
|
||||
|
||||
const selector = new SessionSelectorComponent(
|
||||
sessionManager,
|
||||
sessions,
|
||||
(path: string) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
|
|
|
|||
|
|
@ -106,12 +106,8 @@ export interface CreateAgentSessionOptions {
|
|||
/** Slash commands. Default: discovered from cwd/.pi/commands/ + agentDir/commands/ */
|
||||
slashCommands?: FileSlashCommand[];
|
||||
|
||||
/** Session file path, or false to disable persistence. Default: auto in agentDir/sessions/ */
|
||||
sessionFile?: string | false;
|
||||
/** Continue most recent session for cwd. */
|
||||
continueSession?: boolean;
|
||||
/** Restore model/thinking from session (default: true when continuing). */
|
||||
restoreFromSession?: boolean;
|
||||
/** Session manager. Default: SessionManager.create(cwd) */
|
||||
sessionManager?: SessionManager;
|
||||
|
||||
/** Settings overrides (merged with agentDir/settings.json) */
|
||||
settings?: Partial<Settings>;
|
||||
|
|
@ -411,7 +407,7 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
|||
* tools: [readTool, bashTool],
|
||||
* hooks: [],
|
||||
* skills: [],
|
||||
* sessionFile: false,
|
||||
* sessionManager: SessionManager.inMemory(),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
|
@ -420,34 +416,27 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
const agentDir = options.agentDir ?? getDefaultAgentDir();
|
||||
|
||||
const settingsManager = new SettingsManager(agentDir);
|
||||
const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
|
||||
|
||||
const sessionManager = new SessionManager(options.continueSession ?? false, undefined);
|
||||
if (options.sessionFile === false) {
|
||||
sessionManager.disable();
|
||||
} else if (typeof options.sessionFile === "string") {
|
||||
sessionManager.setSessionFile(options.sessionFile);
|
||||
}
|
||||
// Check if session has existing data to restore
|
||||
const existingSession = sessionManager.loadSession();
|
||||
const hasExistingSession = existingSession.messages.length > 0;
|
||||
|
||||
let model = options.model;
|
||||
let modelFallbackMessage: string | undefined;
|
||||
const shouldRestoreFromSession = options.restoreFromSession ?? (options.continueSession || options.sessionFile);
|
||||
|
||||
// If continuing/restoring, try to get model from session first
|
||||
if (!model && shouldRestoreFromSession) {
|
||||
const savedModel = sessionManager.loadModel();
|
||||
if (savedModel) {
|
||||
const restoredModel = findModel(savedModel.provider, savedModel.modelId);
|
||||
if (restoredModel) {
|
||||
const key = await getApiKeyForModel(restoredModel);
|
||||
if (key) {
|
||||
model = restoredModel;
|
||||
}
|
||||
}
|
||||
// If we couldn't restore, we'll fall back below and set fallback message
|
||||
if (!model) {
|
||||
modelFallbackMessage = `Could not restore model ${savedModel.provider}/${savedModel.modelId}`;
|
||||
// If session has data, try to restore model from it
|
||||
if (!model && hasExistingSession && existingSession.model) {
|
||||
const restoredModel = findModel(existingSession.model.provider, existingSession.model.modelId);
|
||||
if (restoredModel) {
|
||||
const key = await getApiKeyForModel(restoredModel);
|
||||
if (key) {
|
||||
model = restoredModel;
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If still no model, try settings default
|
||||
|
|
@ -482,12 +471,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
|
||||
let thinkingLevel = options.thinkingLevel;
|
||||
|
||||
// If continuing/restoring, try to get thinking level from session
|
||||
if (thinkingLevel === undefined && shouldRestoreFromSession) {
|
||||
const savedThinking = sessionManager.loadThinkingLevel();
|
||||
if (savedThinking) {
|
||||
thinkingLevel = savedThinking as ThinkingLevel;
|
||||
}
|
||||
// If session has data, restore thinking level from it
|
||||
if (thinkingLevel === undefined && hasExistingSession) {
|
||||
thinkingLevel = existingSession.thinkingLevel as ThinkingLevel;
|
||||
}
|
||||
|
||||
// Fall back to settings default
|
||||
|
|
@ -595,11 +581,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
}),
|
||||
});
|
||||
|
||||
if (shouldRestoreFromSession) {
|
||||
const messages = sessionManager.loadMessages();
|
||||
if (messages.length > 0) {
|
||||
agent.replaceMessages(messages);
|
||||
}
|
||||
// Restore messages if session has existing data
|
||||
if (hasExistingSession) {
|
||||
agent.replaceMessages(existingSession.messages);
|
||||
}
|
||||
|
||||
const session = new AgentSession({
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ function uuidv4(): string {
|
|||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session entry types
|
||||
// ============================================================================
|
||||
|
||||
export interface SessionHeader {
|
||||
type: "session";
|
||||
id: string;
|
||||
|
|
@ -50,11 +46,10 @@ export interface CompactionEntry {
|
|||
type: "compaction";
|
||||
timestamp: string;
|
||||
summary: string;
|
||||
firstKeptEntryIndex: number; // Index into session entries where we start keeping
|
||||
firstKeptEntryIndex: number;
|
||||
tokensBefore: number;
|
||||
}
|
||||
|
||||
/** Union of all session entry types */
|
||||
export type SessionEntry =
|
||||
| SessionHeader
|
||||
| SessionMessageEntry
|
||||
|
|
@ -62,16 +57,22 @@ export type SessionEntry =
|
|||
| ModelChangeEntry
|
||||
| CompactionEntry;
|
||||
|
||||
// ============================================================================
|
||||
// Session loading with compaction support
|
||||
// ============================================================================
|
||||
|
||||
export interface LoadedSession {
|
||||
messages: AppMessage[];
|
||||
thinkingLevel: string;
|
||||
model: { provider: string; modelId: string } | null;
|
||||
}
|
||||
|
||||
export interface SessionInfo {
|
||||
path: string;
|
||||
id: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
messageCount: number;
|
||||
firstMessage: string;
|
||||
allMessagesText: string;
|
||||
}
|
||||
|
||||
export const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
|
||||
|
||||
<summary>
|
||||
|
|
@ -80,9 +81,6 @@ export const SUMMARY_PREFIX = `The conversation history before this point was co
|
|||
export const SUMMARY_SUFFIX = `
|
||||
</summary>`;
|
||||
|
||||
/**
|
||||
* Create a user message containing the summary with the standard prefix.
|
||||
*/
|
||||
export function createSummaryMessage(summary: string): AppMessage {
|
||||
return {
|
||||
role: "user",
|
||||
|
|
@ -91,9 +89,6 @@ export function createSummaryMessage(summary: string): AppMessage {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse session file content into entries.
|
||||
*/
|
||||
export function parseSessionEntries(content: string): SessionEntry[] {
|
||||
const entries: SessionEntry[] = [];
|
||||
const lines = content.trim().split("\n");
|
||||
|
|
@ -111,17 +106,6 @@ export function parseSessionEntries(content: string): SessionEntry[] {
|
|||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load session from entries, handling compaction events.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Find latest compaction event (if any)
|
||||
* 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)
|
||||
* 3. Prepend summary as user message
|
||||
*/
|
||||
/**
|
||||
* Get the latest compaction entry from session entries, if any.
|
||||
*/
|
||||
export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
if (entries[i].type === "compaction") {
|
||||
|
|
@ -132,7 +116,6 @@ export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEnt
|
|||
}
|
||||
|
||||
export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
||||
// Find model and thinking level (always scan all entries)
|
||||
let thinkingLevel = "off";
|
||||
let model: { provider: string; modelId: string } | null = null;
|
||||
|
||||
|
|
@ -147,7 +130,6 @@ export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
|||
}
|
||||
}
|
||||
|
||||
// Find latest compaction event
|
||||
let latestCompactionIndex = -1;
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
if (entries[i].type === "compaction") {
|
||||
|
|
@ -156,7 +138,6 @@ export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
|||
}
|
||||
}
|
||||
|
||||
// No compaction: return all messages
|
||||
if (latestCompactionIndex === -1) {
|
||||
const messages: AppMessage[] = [];
|
||||
for (const entry of entries) {
|
||||
|
|
@ -169,7 +150,6 @@ export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
|||
|
||||
const compactionEvent = entries[latestCompactionIndex] as CompactionEntry;
|
||||
|
||||
// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)
|
||||
const keptMessages: AppMessage[] = [];
|
||||
for (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
|
|
@ -178,7 +158,6 @@ export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
|||
}
|
||||
}
|
||||
|
||||
// Build final messages: summary + kept messages
|
||||
const messages: AppMessage[] = [];
|
||||
messages.push(createSummaryMessage(compactionEvent.summary));
|
||||
messages.push(...keptMessages);
|
||||
|
|
@ -186,320 +165,137 @@ export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
|||
return { messages, thinkingLevel, model };
|
||||
}
|
||||
|
||||
function getSessionDirectory(cwd: string): string {
|
||||
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
||||
const configDir = getAgentDir();
|
||||
const sessionDir = join(configDir, "sessions", safePath);
|
||||
if (!existsSync(sessionDir)) {
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
}
|
||||
return sessionDir;
|
||||
}
|
||||
|
||||
function loadEntriesFromFile(filePath: string): SessionEntry[] {
|
||||
if (!existsSync(filePath)) return [];
|
||||
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const entries: SessionEntry[] = [];
|
||||
const lines = content.trim().split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const entry = JSON.parse(line) as SessionEntry;
|
||||
entries.push(entry);
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function extractSessionIdFromFile(filePath: string): string | null {
|
||||
if (!existsSync(filePath)) return null;
|
||||
|
||||
const lines = readFileSync(filePath, "utf8").trim().split("\n");
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.type === "session") {
|
||||
return entry.id;
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findMostRecentSession(sessionDir: string): string | null {
|
||||
try {
|
||||
const files = readdirSync(sessionDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.map((f) => ({
|
||||
path: join(sessionDir, f),
|
||||
mtime: statSync(join(sessionDir, f)).mtime,
|
||||
}))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
return files[0]?.path || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionManager {
|
||||
private sessionId!: string;
|
||||
private sessionFile!: string;
|
||||
private sessionId: string;
|
||||
private sessionFile: string;
|
||||
private sessionDir: string;
|
||||
private enabled: boolean = true;
|
||||
private sessionInitialized: boolean = false;
|
||||
private cwd: string;
|
||||
private enabled: boolean;
|
||||
private sessionInitialized: boolean;
|
||||
private pendingEntries: SessionEntry[] = [];
|
||||
// In-memory entries for --no-session mode (when enabled=false)
|
||||
private inMemoryEntries: SessionEntry[] = [];
|
||||
|
||||
constructor(continueSession: boolean = false, customSessionPath?: string) {
|
||||
this.sessionDir = this.getSessionDirectory();
|
||||
private constructor(cwd: string, sessionFile: string | null, enabled: boolean) {
|
||||
this.cwd = cwd;
|
||||
this.sessionDir = getSessionDirectory(cwd);
|
||||
this.enabled = enabled;
|
||||
|
||||
if (customSessionPath) {
|
||||
// Use custom session file path
|
||||
this.sessionFile = resolve(customSessionPath);
|
||||
this.loadSessionId();
|
||||
// If file doesn't exist, loadSessionId() won't set sessionId, so generate one
|
||||
if (!this.sessionId) {
|
||||
this.sessionId = uuidv4();
|
||||
}
|
||||
// Mark as initialized since we're loading an existing session
|
||||
if (sessionFile) {
|
||||
this.sessionFile = resolve(sessionFile);
|
||||
this.sessionId = extractSessionIdFromFile(this.sessionFile) ?? uuidv4();
|
||||
this.sessionInitialized = existsSync(this.sessionFile);
|
||||
// Load entries into memory
|
||||
if (this.sessionInitialized) {
|
||||
this.inMemoryEntries = this.loadEntriesFromFile();
|
||||
}
|
||||
} else if (continueSession) {
|
||||
const mostRecent = this.findMostRecentlyModifiedSession();
|
||||
if (mostRecent) {
|
||||
this.sessionFile = mostRecent;
|
||||
this.loadSessionId();
|
||||
// Mark as initialized since we're loading an existing session
|
||||
this.sessionInitialized = true;
|
||||
// Load entries into memory
|
||||
this.inMemoryEntries = this.loadEntriesFromFile();
|
||||
} else {
|
||||
this.initNewSession();
|
||||
this.inMemoryEntries = loadEntriesFromFile(this.sessionFile);
|
||||
}
|
||||
} else {
|
||||
this.initNewSession();
|
||||
this.sessionId = uuidv4();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
this.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
|
||||
this.sessionInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Disable session saving (for --no-session mode) */
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
/** Create a new session for the given directory */
|
||||
static create(cwd: string): SessionManager {
|
||||
return new SessionManager(cwd, null, true);
|
||||
}
|
||||
|
||||
/** Check if session persistence is enabled */
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
/** Open a specific session file */
|
||||
static open(path: string): SessionManager {
|
||||
// Extract cwd from session header if possible, otherwise use process.cwd()
|
||||
const entries = loadEntriesFromFile(path);
|
||||
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
|
||||
const cwd = header?.cwd ?? process.cwd();
|
||||
return new SessionManager(cwd, path, true);
|
||||
}
|
||||
|
||||
private getSessionDirectory(): string {
|
||||
const cwd = process.cwd();
|
||||
// Replace all path separators and colons (for Windows drive letters) with dashes
|
||||
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
||||
|
||||
const configDir = getAgentDir();
|
||||
const sessionDir = join(configDir, "sessions", safePath);
|
||||
if (!existsSync(sessionDir)) {
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
/** Continue the most recent session for the given directory, or create new if none */
|
||||
static continueRecent(cwd: string): SessionManager {
|
||||
const sessionDir = getSessionDirectory(cwd);
|
||||
const mostRecent = findMostRecentSession(sessionDir);
|
||||
if (mostRecent) {
|
||||
return new SessionManager(cwd, mostRecent, true);
|
||||
}
|
||||
return sessionDir;
|
||||
return new SessionManager(cwd, null, true);
|
||||
}
|
||||
|
||||
private initNewSession(): void {
|
||||
this.sessionId = uuidv4();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
this.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
|
||||
/** Create an in-memory session (no file persistence) */
|
||||
static inMemory(): SessionManager {
|
||||
return new SessionManager(process.cwd(), null, false);
|
||||
}
|
||||
|
||||
/** Reset to a fresh session. Clears pending entries and starts a new session file. */
|
||||
reset(): void {
|
||||
this.pendingEntries = [];
|
||||
this.inMemoryEntries = [];
|
||||
this.sessionInitialized = false;
|
||||
this.initNewSession();
|
||||
}
|
||||
|
||||
private findMostRecentlyModifiedSession(): string | null {
|
||||
try {
|
||||
const files = readdirSync(this.sessionDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.map((f) => ({
|
||||
name: f,
|
||||
path: join(this.sessionDir, f),
|
||||
mtime: statSync(join(this.sessionDir, f)).mtime,
|
||||
}))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
return files[0]?.path || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private loadSessionId(): void {
|
||||
if (!existsSync(this.sessionFile)) return;
|
||||
|
||||
const lines = readFileSync(this.sessionFile, "utf8").trim().split("\n");
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.type === "session") {
|
||||
this.sessionId = entry.id;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
this.sessionId = uuidv4();
|
||||
}
|
||||
|
||||
startSession(state: AgentState): void {
|
||||
if (this.sessionInitialized) return;
|
||||
this.sessionInitialized = true;
|
||||
|
||||
const entry: SessionHeader = {
|
||||
type: "session",
|
||||
id: this.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
provider: state.model.provider,
|
||||
modelId: state.model.id,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
};
|
||||
|
||||
// Always track in memory
|
||||
this.inMemoryEntries.push(entry);
|
||||
for (const pending of this.pendingEntries) {
|
||||
this.inMemoryEntries.push(pending);
|
||||
}
|
||||
this.pendingEntries = [];
|
||||
|
||||
// Write to file only if enabled
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
for (const memEntry of this.inMemoryEntries.slice(1)) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(memEntry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveMessage(message: any): void {
|
||||
const entry: SessionMessageEntry = {
|
||||
type: "message",
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
};
|
||||
|
||||
if (!this.sessionInitialized) {
|
||||
this.pendingEntries.push(entry);
|
||||
} else {
|
||||
// Always track in memory
|
||||
this.inMemoryEntries.push(entry);
|
||||
// Write to file only if enabled
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveThinkingLevelChange(thinkingLevel: string): void {
|
||||
const entry: ThinkingLevelChangeEntry = {
|
||||
type: "thinking_level_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
thinkingLevel,
|
||||
};
|
||||
|
||||
if (!this.sessionInitialized) {
|
||||
this.pendingEntries.push(entry);
|
||||
} else {
|
||||
// Always track in memory
|
||||
this.inMemoryEntries.push(entry);
|
||||
// Write to file only if enabled
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveModelChange(provider: string, modelId: string): void {
|
||||
const entry: ModelChangeEntry = {
|
||||
type: "model_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
provider,
|
||||
modelId,
|
||||
};
|
||||
|
||||
if (!this.sessionInitialized) {
|
||||
this.pendingEntries.push(entry);
|
||||
} else {
|
||||
// Always track in memory
|
||||
this.inMemoryEntries.push(entry);
|
||||
// Write to file only if enabled
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveCompaction(entry: CompactionEntry): void {
|
||||
// Always track in memory
|
||||
this.inMemoryEntries.push(entry);
|
||||
// Write to file only if enabled
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load session data (messages, model, thinking level) with compaction support.
|
||||
*/
|
||||
loadSession(): LoadedSession {
|
||||
const entries = this.loadEntries();
|
||||
return loadSessionFromEntries(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use loadSession().messages instead
|
||||
*/
|
||||
loadMessages(): AppMessage[] {
|
||||
return this.loadSession().messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use loadSession().thinkingLevel instead
|
||||
*/
|
||||
loadThinkingLevel(): string {
|
||||
return this.loadSession().thinkingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use loadSession().model instead
|
||||
*/
|
||||
loadModel(): { provider: string; modelId: string } | null {
|
||||
return this.loadSession().model;
|
||||
}
|
||||
|
||||
getSessionId(): string {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
getSessionFile(): string {
|
||||
return this.sessionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load entries directly from the session file (internal helper).
|
||||
*/
|
||||
private loadEntriesFromFile(): SessionEntry[] {
|
||||
if (!existsSync(this.sessionFile)) return [];
|
||||
|
||||
const content = readFileSync(this.sessionFile, "utf8");
|
||||
const entries: SessionEntry[] = [];
|
||||
const lines = content.trim().split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const entry = JSON.parse(line) as SessionEntry;
|
||||
entries.push(entry);
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all entries from the session file or in-memory store.
|
||||
* When file persistence is enabled, reads from file (source of truth for resumed sessions).
|
||||
* When disabled (--no-session), returns in-memory entries.
|
||||
*/
|
||||
loadEntries(): SessionEntry[] {
|
||||
// If file persistence is enabled and file exists, read from file
|
||||
if (this.enabled && existsSync(this.sessionFile)) {
|
||||
return this.loadEntriesFromFile();
|
||||
}
|
||||
|
||||
// Otherwise return in-memory entries (for --no-session mode)
|
||||
return [...this.inMemoryEntries];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all sessions for the current directory with metadata
|
||||
*/
|
||||
loadAllSessions(): Array<{
|
||||
path: string;
|
||||
id: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
messageCount: number;
|
||||
firstMessage: string;
|
||||
allMessagesText: string;
|
||||
}> {
|
||||
const sessions: Array<{
|
||||
path: string;
|
||||
id: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
messageCount: number;
|
||||
firstMessage: string;
|
||||
allMessagesText: string;
|
||||
}> = [];
|
||||
/** List all sessions for a directory */
|
||||
static list(cwd: string): SessionInfo[] {
|
||||
const sessionDir = getSessionDirectory(cwd);
|
||||
const sessions: SessionInfo[] = [];
|
||||
|
||||
try {
|
||||
const files = readdirSync(this.sessionDir)
|
||||
const files = readdirSync(sessionDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.map((f) => join(this.sessionDir, f));
|
||||
.map((f) => join(sessionDir, f));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
|
|
@ -517,17 +313,14 @@ export class SessionManager {
|
|||
try {
|
||||
const entry = JSON.parse(line);
|
||||
|
||||
// Extract session ID from first session entry
|
||||
if (entry.type === "session" && !sessionId) {
|
||||
sessionId = entry.id;
|
||||
created = new Date(entry.timestamp);
|
||||
}
|
||||
|
||||
// Count messages and collect all text
|
||||
if (entry.type === "message") {
|
||||
messageCount++;
|
||||
|
||||
// Extract text from user and assistant messages
|
||||
if (entry.message.role === "user" || entry.message.role === "assistant") {
|
||||
const textContent = entry.message.content
|
||||
.filter((c: any) => c.type === "text")
|
||||
|
|
@ -537,7 +330,6 @@ export class SessionManager {
|
|||
if (textContent) {
|
||||
allMessages.push(textContent);
|
||||
|
||||
// Get first user message for display
|
||||
if (!firstMessage && entry.message.role === "user") {
|
||||
firstMessage = textContent;
|
||||
}
|
||||
|
|
@ -558,42 +350,168 @@ export class SessionManager {
|
|||
firstMessage: firstMessage || "(no messages)",
|
||||
allMessagesText: allMessages.join(" "),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Skip files that can't be read
|
||||
console.error(`Failed to read session file ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by modified date (most recent first)
|
||||
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
} catch (error) {
|
||||
console.error("Failed to load sessions:", error);
|
||||
} catch {
|
||||
// Return empty list on error
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the session file to an existing session
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
getCwd(): string {
|
||||
return this.cwd;
|
||||
}
|
||||
|
||||
getSessionId(): string {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
getSessionFile(): string {
|
||||
return this.sessionFile;
|
||||
}
|
||||
|
||||
/** Switch to a different session file (used for resume and branching) */
|
||||
setSessionFile(path: string): void {
|
||||
this.sessionFile = path;
|
||||
this.loadSessionId();
|
||||
// Mark as initialized since we're loading an existing session
|
||||
this.sessionInitialized = existsSync(path);
|
||||
// Load entries into memory for consistency
|
||||
this.sessionFile = resolve(path);
|
||||
this.sessionId = extractSessionIdFromFile(this.sessionFile) ?? uuidv4();
|
||||
this.sessionInitialized = existsSync(this.sessionFile);
|
||||
if (this.sessionInitialized) {
|
||||
this.inMemoryEntries = this.loadEntriesFromFile();
|
||||
this.inMemoryEntries = loadEntriesFromFile(this.sessionFile);
|
||||
} else {
|
||||
this.inMemoryEntries = [];
|
||||
}
|
||||
this.pendingEntries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should initialize the session based on message history.
|
||||
* Session is initialized when we have at least 1 user message and 1 assistant message.
|
||||
*/
|
||||
reset(): void {
|
||||
this.pendingEntries = [];
|
||||
this.inMemoryEntries = [];
|
||||
this.sessionInitialized = false;
|
||||
this.sessionId = uuidv4();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
this.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
startSession(state: AgentState): void {
|
||||
if (this.sessionInitialized) return;
|
||||
this.sessionInitialized = true;
|
||||
|
||||
const entry: SessionHeader = {
|
||||
type: "session",
|
||||
id: this.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: this.cwd,
|
||||
provider: state.model.provider,
|
||||
modelId: state.model.id,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
};
|
||||
|
||||
this.inMemoryEntries.push(entry);
|
||||
for (const pending of this.pendingEntries) {
|
||||
this.inMemoryEntries.push(pending);
|
||||
}
|
||||
this.pendingEntries = [];
|
||||
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
for (const memEntry of this.inMemoryEntries.slice(1)) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(memEntry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveMessage(message: any): void {
|
||||
const entry: SessionMessageEntry = {
|
||||
type: "message",
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
};
|
||||
|
||||
if (!this.sessionInitialized) {
|
||||
this.pendingEntries.push(entry);
|
||||
} else {
|
||||
this.inMemoryEntries.push(entry);
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveThinkingLevelChange(thinkingLevel: string): void {
|
||||
const entry: ThinkingLevelChangeEntry = {
|
||||
type: "thinking_level_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
thinkingLevel,
|
||||
};
|
||||
|
||||
if (!this.sessionInitialized) {
|
||||
this.pendingEntries.push(entry);
|
||||
} else {
|
||||
this.inMemoryEntries.push(entry);
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveModelChange(provider: string, modelId: string): void {
|
||||
const entry: ModelChangeEntry = {
|
||||
type: "model_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
provider,
|
||||
modelId,
|
||||
};
|
||||
|
||||
if (!this.sessionInitialized) {
|
||||
this.pendingEntries.push(entry);
|
||||
} else {
|
||||
this.inMemoryEntries.push(entry);
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveCompaction(entry: CompactionEntry): void {
|
||||
this.inMemoryEntries.push(entry);
|
||||
if (this.enabled) {
|
||||
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
loadSession(): LoadedSession {
|
||||
const entries = this.loadEntries();
|
||||
return loadSessionFromEntries(entries);
|
||||
}
|
||||
|
||||
loadMessages(): AppMessage[] {
|
||||
return this.loadSession().messages;
|
||||
}
|
||||
|
||||
loadThinkingLevel(): string {
|
||||
return this.loadSession().thinkingLevel;
|
||||
}
|
||||
|
||||
loadModel(): { provider: string; modelId: string } | null {
|
||||
return this.loadSession().model;
|
||||
}
|
||||
|
||||
loadEntries(): SessionEntry[] {
|
||||
if (this.enabled && existsSync(this.sessionFile)) {
|
||||
return loadEntriesFromFile(this.sessionFile);
|
||||
}
|
||||
return [...this.inMemoryEntries];
|
||||
}
|
||||
|
||||
shouldInitializeSession(messages: any[]): boolean {
|
||||
if (this.sessionInitialized) return false;
|
||||
|
||||
|
|
@ -603,23 +521,16 @@ export class SessionManager {
|
|||
return userMessages.length >= 1 && assistantMessages.length >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a branched session from a specific message index.
|
||||
* If branchFromIndex is -1, creates an empty session.
|
||||
* Returns the new session file path.
|
||||
*/
|
||||
createBranchedSession(state: any, branchFromIndex: number): string {
|
||||
// Create a new session ID for the branch
|
||||
const newSessionId = uuidv4();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);
|
||||
|
||||
// Write session header
|
||||
const entry: SessionHeader = {
|
||||
type: "session",
|
||||
id: newSessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
cwd: this.cwd,
|
||||
provider: state.model.provider,
|
||||
modelId: state.model.id,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
|
|
@ -627,7 +538,6 @@ export class SessionManager {
|
|||
};
|
||||
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
|
||||
|
||||
// Write messages up to and including the branch point (if >= 0)
|
||||
if (branchFromIndex >= 0) {
|
||||
const messagesToWrite = state.messages.slice(0, branchFromIndex + 1);
|
||||
for (const message of messagesToWrite) {
|
||||
|
|
@ -643,23 +553,16 @@ export class SessionManager {
|
|||
return newSessionFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, or null if in --no-session mode (in-memory only).
|
||||
*/
|
||||
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`);
|
||||
|
||||
// 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
|
||||
newEntries.push({
|
||||
...entry,
|
||||
id: newSessionId,
|
||||
|
|
@ -667,22 +570,18 @@ export class SessionManager {
|
|||
branchedFrom: this.enabled ? this.sessionFile : undefined,
|
||||
});
|
||||
} else {
|
||||
// Copy other entries as-is
|
||||
newEntries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
this.inMemoryEntries = newEntries;
|
||||
this.sessionId = newSessionId;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export {
|
|||
parseSessionEntries,
|
||||
type SessionEntry,
|
||||
type SessionHeader,
|
||||
type SessionInfo,
|
||||
SessionManager,
|
||||
type SessionMessageEntry,
|
||||
SUMMARY_PREFIX,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"
|
|||
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js";
|
||||
import { ensureTool } from "./utils/tools-manager.js";
|
||||
|
||||
/** Configure OAuth storage to use the coding-agent's configurable path */
|
||||
function configureOAuthStorage(): void {
|
||||
const oauthPath = getOAuthPath();
|
||||
|
||||
|
|
@ -55,7 +54,6 @@ function configureOAuthStorage(): void {
|
|||
});
|
||||
}
|
||||
|
||||
/** Check npm registry for new version (non-blocking) */
|
||||
async function checkForNewVersion(currentVersion: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest");
|
||||
|
|
@ -74,7 +72,6 @@ async function checkForNewVersion(currentVersion: string): Promise<string | null
|
|||
}
|
||||
}
|
||||
|
||||
/** Run interactive mode with TUI */
|
||||
async function runInteractiveMode(
|
||||
session: AgentSession,
|
||||
version: string,
|
||||
|
|
@ -133,7 +130,6 @@ async function runInteractiveMode(
|
|||
}
|
||||
}
|
||||
|
||||
/** Prepare initial message from @file arguments */
|
||||
async function prepareInitialMessage(parsed: Args): Promise<{
|
||||
initialMessage?: string;
|
||||
initialAttachments?: Attachment[];
|
||||
|
|
@ -158,7 +154,6 @@ async function prepareInitialMessage(parsed: Args): Promise<{
|
|||
};
|
||||
}
|
||||
|
||||
/** Get changelog markdown to display (only for new sessions with updates) */
|
||||
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | null {
|
||||
if (parsed.continue || parsed.resume) {
|
||||
return null;
|
||||
|
|
@ -184,10 +179,32 @@ function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager):
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Build CreateAgentSessionOptions from CLI args */
|
||||
async function buildSessionOptions(parsed: Args, scopedModels: ScopedModel[]): Promise<CreateAgentSessionOptions> {
|
||||
function createSessionManager(parsed: Args, cwd: string): SessionManager | null {
|
||||
if (parsed.noSession) {
|
||||
return SessionManager.inMemory();
|
||||
}
|
||||
if (parsed.session) {
|
||||
return SessionManager.open(parsed.session);
|
||||
}
|
||||
if (parsed.continue) {
|
||||
return SessionManager.continueRecent(cwd);
|
||||
}
|
||||
// --resume is handled separately (needs picker UI)
|
||||
// Default case (new session) returns null, SDK will create one
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildSessionOptions(
|
||||
parsed: Args,
|
||||
scopedModels: ScopedModel[],
|
||||
sessionManager: SessionManager | null,
|
||||
): CreateAgentSessionOptions {
|
||||
const options: CreateAgentSessionOptions = {};
|
||||
|
||||
if (sessionManager) {
|
||||
options.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
// Model from CLI
|
||||
if (parsed.provider && parsed.model) {
|
||||
const { model, error } = findModel(parsed.provider, parsed.model);
|
||||
|
|
@ -201,7 +218,6 @@ async function buildSessionOptions(parsed: Args, scopedModels: ScopedModel[]): P
|
|||
}
|
||||
options.model = model;
|
||||
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
||||
// Use first scoped model
|
||||
options.model = scopedModels[0].model;
|
||||
}
|
||||
|
||||
|
|
@ -251,23 +267,10 @@ async function buildSessionOptions(parsed: Args, scopedModels: ScopedModel[]): P
|
|||
options.additionalCustomToolPaths = parsed.customTools;
|
||||
}
|
||||
|
||||
// Session handling
|
||||
if (parsed.noSession) {
|
||||
options.sessionFile = false;
|
||||
} else if (parsed.session) {
|
||||
options.sessionFile = parsed.session;
|
||||
}
|
||||
|
||||
// Continue session
|
||||
if (parsed.continue && !parsed.resume) {
|
||||
options.continueSession = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function main(args: string[]) {
|
||||
// Configure OAuth storage first
|
||||
configureOAuthStorage();
|
||||
|
||||
const parsed = parseArgs(args);
|
||||
|
|
@ -306,40 +309,38 @@ export async function main(args: string[]) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const { initialMessage, initialAttachments } = await prepareInitialMessage(parsed);
|
||||
const isInteractive = !parsed.print && parsed.mode === undefined;
|
||||
const mode = parsed.mode || "text";
|
||||
|
||||
// Initialize theme early
|
||||
const settingsManager = new SettingsManager();
|
||||
initTheme(settingsManager.getTheme(), isInteractive);
|
||||
|
||||
// Resolve scoped models from --models flag
|
||||
let scopedModels: ScopedModel[] = [];
|
||||
if (parsed.models && parsed.models.length > 0) {
|
||||
scopedModels = await resolveModelScope(parsed.models);
|
||||
}
|
||||
|
||||
// Create session manager based on CLI flags
|
||||
let sessionManager = createSessionManager(parsed, cwd);
|
||||
|
||||
// Handle --resume: show session picker
|
||||
let sessionFileFromResume: string | undefined;
|
||||
if (parsed.resume) {
|
||||
const tempSessionManager = new SessionManager(false, undefined);
|
||||
const selectedSession = await selectSession(tempSessionManager);
|
||||
if (!selectedSession) {
|
||||
const sessions = SessionManager.list(cwd);
|
||||
if (sessions.length === 0) {
|
||||
console.log(chalk.dim("No sessions found"));
|
||||
return;
|
||||
}
|
||||
const selectedPath = await selectSession(sessions);
|
||||
if (!selectedPath) {
|
||||
console.log(chalk.dim("No session selected"));
|
||||
return;
|
||||
}
|
||||
sessionFileFromResume = selectedSession;
|
||||
}
|
||||
|
||||
const sessionOptions = await buildSessionOptions(parsed, scopedModels);
|
||||
|
||||
// Apply resume session file
|
||||
if (sessionFileFromResume) {
|
||||
sessionOptions.sessionFile = sessionFileFromResume;
|
||||
sessionOptions.restoreFromSession = true;
|
||||
sessionManager = SessionManager.open(selectedPath);
|
||||
}
|
||||
|
||||
const sessionOptions = buildSessionOptions(parsed, scopedModels, sessionManager);
|
||||
const { session, customToolsResult, modelFallbackMessage } = await createAgentSession(sessionOptions);
|
||||
|
||||
if (!isInteractive && !session.model) {
|
||||
|
|
@ -369,7 +370,6 @@ export async function main(args: string[]) {
|
|||
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => null);
|
||||
const changelogMarkdown = getChangelogForDisplay(parsed, settingsManager);
|
||||
|
||||
// Show model scope if provided
|
||||
if (scopedModels.length > 0) {
|
||||
const modelList = scopedModels
|
||||
.map((sm) => {
|
||||
|
|
|
|||
|
|
@ -11,27 +11,17 @@ import {
|
|||
Text,
|
||||
truncateToWidth,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import type { SessionManager } from "../../../core/session-manager.js";
|
||||
import type { SessionInfo } from "../../../core/session-manager.js";
|
||||
import { fuzzyFilter } from "../../../utils/fuzzy.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
interface SessionItem {
|
||||
path: string;
|
||||
id: string;
|
||||
created: Date;
|
||||
modified: Date;
|
||||
messageCount: number;
|
||||
firstMessage: string;
|
||||
allMessagesText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom session list component with multi-line items and search
|
||||
*/
|
||||
class SessionList implements Component {
|
||||
private allSessions: SessionItem[] = [];
|
||||
private filteredSessions: SessionItem[] = [];
|
||||
private allSessions: SessionInfo[] = [];
|
||||
private filteredSessions: SessionInfo[] = [];
|
||||
private selectedIndex: number = 0;
|
||||
private searchInput: Input;
|
||||
public onSelect?: (sessionPath: string) => void;
|
||||
|
|
@ -39,7 +29,7 @@ class SessionList implements Component {
|
|||
public onExit: () => void = () => {};
|
||||
private maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
|
||||
|
||||
constructor(sessions: SessionItem[]) {
|
||||
constructor(sessions: SessionInfo[]) {
|
||||
this.allSessions = sessions;
|
||||
this.filteredSessions = sessions;
|
||||
this.searchInput = new Input();
|
||||
|
|
@ -176,16 +166,13 @@ export class SessionSelectorComponent extends Container {
|
|||
private sessionList: SessionList;
|
||||
|
||||
constructor(
|
||||
sessionManager: SessionManager,
|
||||
sessions: SessionInfo[],
|
||||
onSelect: (sessionPath: string) => void,
|
||||
onCancel: () => void,
|
||||
onExit: () => void,
|
||||
) {
|
||||
super();
|
||||
|
||||
// Load all sessions
|
||||
const sessions = sessionManager.loadAllSessions();
|
||||
|
||||
// Add header
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.bold("Resume Session"), 1, 0));
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ import type { HookUIContext } from "../../core/hooks/index.js";
|
|||
import { isBashExecutionMessage } from "../../core/messages.js";
|
||||
import { invalidateOAuthCache } from "../../core/model-config.js";
|
||||
import { listOAuthProviders, login, logout, type OAuthProvider } from "../../core/oauth/index.js";
|
||||
import { getLatestCompactionEntry, SUMMARY_PREFIX, SUMMARY_SUFFIX } from "../../core/session-manager.js";
|
||||
import {
|
||||
getLatestCompactionEntry,
|
||||
SessionManager,
|
||||
SUMMARY_PREFIX,
|
||||
SUMMARY_SUFFIX,
|
||||
} from "../../core/session-manager.js";
|
||||
import { loadSkills } from "../../core/skills.js";
|
||||
import { loadProjectContextFiles } from "../../core/system-prompt.js";
|
||||
import type { TruncationResult } from "../../core/tools/truncate.js";
|
||||
|
|
@ -1513,8 +1518,9 @@ export class InteractiveMode {
|
|||
|
||||
private showSessionSelector(): void {
|
||||
this.showSelector((done) => {
|
||||
const sessions = SessionManager.list(this.sessionManager.getCwd());
|
||||
const selector = new SessionSelectorComponent(
|
||||
this.sessionManager,
|
||||
sessions,
|
||||
async (sessionPath) => {
|
||||
done();
|
||||
await this.handleResumeSession(sessionPath);
|
||||
|
|
|
|||
|
|
@ -56,10 +56,7 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
|||
},
|
||||
});
|
||||
|
||||
sessionManager = new SessionManager(false);
|
||||
if (noSession) {
|
||||
sessionManager.disable();
|
||||
}
|
||||
sessionManager = noSession ? SessionManager.inMemory() : SessionManager.create(tempDir);
|
||||
const settingsManager = new SettingsManager(tempDir);
|
||||
|
||||
session = new AgentSession({
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
|||
},
|
||||
});
|
||||
|
||||
sessionManager = new SessionManager(false);
|
||||
sessionManager = SessionManager.create(tempDir);
|
||||
const settingsManager = new SettingsManager(tempDir);
|
||||
|
||||
session = new AgentSession({
|
||||
|
|
@ -174,9 +174,8 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
|||
},
|
||||
});
|
||||
|
||||
// Create session manager and disable file persistence
|
||||
const noSessionManager = new SessionManager(false);
|
||||
noSessionManager.disable();
|
||||
// Create in-memory session manager
|
||||
const noSessionManager = SessionManager.inMemory();
|
||||
|
||||
const settingsManager = new SettingsManager(tempDir);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue