mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 04:01:56 +00:00
refactor(hooks): split session events into individual typed events
Major changes: - Replace monolithic SessionEvent with reason discriminator with individual event types: session_start, session_before_switch, session_switch, session_before_new, session_new, session_before_branch, session_branch, session_before_compact, session_compact, session_shutdown - Each event has dedicated result type (SessionBeforeSwitchResult, etc.) - HookHandler type now allows bare return statements (void in return type) - HookAPI.on() has proper overloads for each event with correct typing Additional fixes: - AgentSession now always subscribes to agent in constructor (was only subscribing when external subscribe() called, breaking internal handlers) - Standardize on undefined over null throughout codebase - HookUIContext methods return undefined instead of null - SessionManager methods return undefined instead of null - Simplify hook exports to 'export type * from types.js' - Add detailed JSDoc for skipConversationRestore vs cancel - Fix createBranchedSession to rebuild index in persist mode - newSession() now returns the session file path Updated all example hooks, tests, and emission sites to use new event types.
This commit is contained in:
parent
38d65dfe59
commit
d6283f99dc
43 changed files with 2129 additions and 640 deletions
|
|
@ -30,7 +30,10 @@ import { exportSessionToHtml } from "./export-html.js";
|
|||
import type {
|
||||
HookCommandContext,
|
||||
HookRunner,
|
||||
SessionEventResult,
|
||||
SessionBeforeBranchResult,
|
||||
SessionBeforeCompactResult,
|
||||
SessionBeforeNewResult,
|
||||
SessionBeforeSwitchResult,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
} from "./hooks/index.js";
|
||||
|
|
@ -44,7 +47,7 @@ import { expandSlashCommand, type FileSlashCommand } from "./slash-commands.js";
|
|||
export type AgentSessionEvent =
|
||||
| AgentEvent
|
||||
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
||||
| { type: "auto_compaction_end"; result: CompactionResult | null; aborted: boolean; willRetry: boolean }
|
||||
| { type: "auto_compaction_end"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean }
|
||||
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
||||
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string };
|
||||
|
||||
|
|
@ -64,7 +67,7 @@ export interface AgentSessionConfig {
|
|||
/** File-based slash commands for expansion */
|
||||
fileCommands?: FileSlashCommand[];
|
||||
/** Hook runner (created in main.ts with wrapped tools) */
|
||||
hookRunner?: HookRunner | null;
|
||||
hookRunner?: HookRunner;
|
||||
/** Custom tools for session lifecycle events */
|
||||
customTools?: LoadedCustomTool[];
|
||||
skillsSettings?: Required<SkillsSettings>;
|
||||
|
|
@ -90,7 +93,7 @@ export interface ModelCycleResult {
|
|||
|
||||
/** Session statistics for /session command */
|
||||
export interface SessionStats {
|
||||
sessionFile: string | null;
|
||||
sessionFile: string | undefined;
|
||||
sessionId: string;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
|
|
@ -138,21 +141,21 @@ export class AgentSession {
|
|||
private _queuedMessages: string[] = [];
|
||||
|
||||
// Compaction state
|
||||
private _compactionAbortController: AbortController | null = null;
|
||||
private _autoCompactionAbortController: AbortController | null = null;
|
||||
private _compactionAbortController: AbortController | undefined = undefined;
|
||||
private _autoCompactionAbortController: AbortController | undefined = undefined;
|
||||
|
||||
// Retry state
|
||||
private _retryAbortController: AbortController | null = null;
|
||||
private _retryAbortController: AbortController | undefined = undefined;
|
||||
private _retryAttempt = 0;
|
||||
private _retryPromise: Promise<void> | null = null;
|
||||
private _retryResolve: (() => void) | null = null;
|
||||
private _retryPromise: Promise<void> | undefined = undefined;
|
||||
private _retryResolve: (() => void) | undefined = undefined;
|
||||
|
||||
// Bash execution state
|
||||
private _bashAbortController: AbortController | null = null;
|
||||
private _bashAbortController: AbortController | undefined = undefined;
|
||||
private _pendingBashMessages: BashExecutionMessage[] = [];
|
||||
|
||||
// Hook system
|
||||
private _hookRunner: HookRunner | null = null;
|
||||
private _hookRunner: HookRunner | undefined = undefined;
|
||||
private _turnIndex = 0;
|
||||
|
||||
// Custom tools for session lifecycle
|
||||
|
|
@ -169,10 +172,14 @@ export class AgentSession {
|
|||
this.settingsManager = config.settingsManager;
|
||||
this._scopedModels = config.scopedModels ?? [];
|
||||
this._fileCommands = config.fileCommands ?? [];
|
||||
this._hookRunner = config.hookRunner ?? null;
|
||||
this._hookRunner = config.hookRunner;
|
||||
this._customTools = config.customTools ?? [];
|
||||
this._skillsSettings = config.skillsSettings;
|
||||
this._modelRegistry = config.modelRegistry;
|
||||
|
||||
// Always subscribe to agent events for internal handling
|
||||
// (session persistence, hooks, auto-compaction, retry logic)
|
||||
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
||||
}
|
||||
|
||||
/** Model registry for API key resolution and model discovery */
|
||||
|
|
@ -192,7 +199,7 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
// Track last assistant message for auto-compaction check
|
||||
private _lastAssistantMessage: AssistantMessage | null = null;
|
||||
private _lastAssistantMessage: AssistantMessage | undefined = undefined;
|
||||
|
||||
/** Internal handler for agent events - shared by subscribe and reconnect */
|
||||
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
||||
|
|
@ -246,7 +253,7 @@ export class AgentSession {
|
|||
// Check auto-retry and auto-compaction after agent completes
|
||||
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = null;
|
||||
this._lastAssistantMessage = undefined;
|
||||
|
||||
// Check for retryable errors first (overloaded, rate limit, server errors)
|
||||
if (this._isRetryableError(msg)) {
|
||||
|
|
@ -272,8 +279,8 @@ export class AgentSession {
|
|||
private _resolveRetry(): void {
|
||||
if (this._retryResolve) {
|
||||
this._retryResolve();
|
||||
this._retryResolve = null;
|
||||
this._retryPromise = null;
|
||||
this._retryResolve = undefined;
|
||||
this._retryPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +294,7 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
/** Find the last assistant message in agent state (including aborted ones) */
|
||||
private _findLastAssistantMessage(): AssistantMessage | null {
|
||||
private _findLastAssistantMessage(): AssistantMessage | undefined {
|
||||
const messages = this.agent.state.messages;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
|
|
@ -295,7 +302,7 @@ export class AgentSession {
|
|||
return msg as AssistantMessage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Emit hook events based on agent events */
|
||||
|
|
@ -334,11 +341,6 @@ export class AgentSession {
|
|||
subscribe(listener: AgentSessionEventListener): () => void {
|
||||
this._eventListeners.push(listener);
|
||||
|
||||
// Set up agent subscription if not already done
|
||||
if (!this._unsubscribeAgent) {
|
||||
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
||||
}
|
||||
|
||||
// Return unsubscribe function for this specific listener
|
||||
return () => {
|
||||
const index = this._eventListeners.indexOf(listener);
|
||||
|
|
@ -387,8 +389,8 @@ export class AgentSession {
|
|||
return this.agent.state;
|
||||
}
|
||||
|
||||
/** Current model (may be null if not yet selected) */
|
||||
get model(): Model<any> | null {
|
||||
/** Current model (may be undefined if not yet selected) */
|
||||
get model(): Model<any> | undefined {
|
||||
return this.agent.state.model;
|
||||
}
|
||||
|
||||
|
|
@ -404,7 +406,7 @@ export class AgentSession {
|
|||
|
||||
/** Whether auto-compaction is currently running */
|
||||
get isCompacting(): boolean {
|
||||
return this._autoCompactionAbortController !== null || this._compactionAbortController !== null;
|
||||
return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;
|
||||
}
|
||||
|
||||
/** All messages including custom types like BashExecutionMessage */
|
||||
|
|
@ -417,9 +419,9 @@ export class AgentSession {
|
|||
return this.agent.getQueueMode();
|
||||
}
|
||||
|
||||
/** Current session file path, or null if sessions are disabled */
|
||||
get sessionFile(): string | null {
|
||||
return this.sessionManager.getSessionFile() ?? null;
|
||||
/** Current session file path, or undefined if sessions are disabled */
|
||||
get sessionFile(): string | undefined {
|
||||
return this.sessionManager.getSessionFile();
|
||||
}
|
||||
|
||||
/** Current session ID */
|
||||
|
|
@ -663,12 +665,11 @@ export class AgentSession {
|
|||
async reset(): Promise<boolean> {
|
||||
const previousSessionFile = this.sessionFile;
|
||||
|
||||
// Emit before_new event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
// Emit session_before_new event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_new")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_new",
|
||||
})) as SessionEventResult | undefined;
|
||||
type: "session_before_new",
|
||||
})) as SessionBeforeNewResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
return false;
|
||||
|
|
@ -682,11 +683,10 @@ export class AgentSession {
|
|||
this._queuedMessages = [];
|
||||
this._reconnectToAgent();
|
||||
|
||||
// Emit session event with reason "new" to hooks
|
||||
// Emit session_new event to hooks
|
||||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "new",
|
||||
type: "session_new",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -722,17 +722,17 @@ export class AgentSession {
|
|||
* Cycle to next/previous model.
|
||||
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
||||
* @param direction - "forward" (default) or "backward"
|
||||
* @returns The new model info, or null if only one model available
|
||||
* @returns The new model info, or undefined if only one model available
|
||||
*/
|
||||
async cycleModel(direction: "forward" | "backward" = "forward"): Promise<ModelCycleResult | null> {
|
||||
async cycleModel(direction: "forward" | "backward" = "forward"): Promise<ModelCycleResult | undefined> {
|
||||
if (this._scopedModels.length > 0) {
|
||||
return this._cycleScopedModel(direction);
|
||||
}
|
||||
return this._cycleAvailableModel(direction);
|
||||
}
|
||||
|
||||
private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | null> {
|
||||
if (this._scopedModels.length <= 1) return null;
|
||||
private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
||||
if (this._scopedModels.length <= 1) return undefined;
|
||||
|
||||
const currentModel = this.model;
|
||||
let currentIndex = this._scopedModels.findIndex((sm) => modelsAreEqual(sm.model, currentModel));
|
||||
|
|
@ -759,9 +759,9 @@ export class AgentSession {
|
|||
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
||||
}
|
||||
|
||||
private async _cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | null> {
|
||||
private async _cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
||||
const availableModels = await this._modelRegistry.getAvailable();
|
||||
if (availableModels.length <= 1) return null;
|
||||
if (availableModels.length <= 1) return undefined;
|
||||
|
||||
const currentModel = this.model;
|
||||
let currentIndex = availableModels.findIndex((m) => modelsAreEqual(m, currentModel));
|
||||
|
|
@ -816,10 +816,10 @@ export class AgentSession {
|
|||
|
||||
/**
|
||||
* Cycle to next thinking level.
|
||||
* @returns New level, or null if model doesn't support thinking
|
||||
* @returns New level, or undefined if model doesn't support thinking
|
||||
*/
|
||||
cycleThinkingLevel(): ThinkingLevel | null {
|
||||
if (!this.supportsThinking()) return null;
|
||||
cycleThinkingLevel(): ThinkingLevel | undefined {
|
||||
if (!this.supportsThinking()) return undefined;
|
||||
|
||||
const levels = this.getAvailableThinkingLevels();
|
||||
const currentIndex = levels.indexOf(this.thinkingLevel);
|
||||
|
|
@ -904,19 +904,18 @@ export class AgentSession {
|
|||
let hookCompaction: CompactionResult | undefined;
|
||||
let fromHook = false;
|
||||
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
if (this._hookRunner?.hasHandlers("session_before_compact")) {
|
||||
// Get previous compactions, newest first
|
||||
const previousCompactions = entries.filter((e): e is CompactionEntry => e.type === "compaction").reverse();
|
||||
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_compact",
|
||||
type: "session_before_compact",
|
||||
preparation,
|
||||
previousCompactions,
|
||||
customInstructions,
|
||||
model: this.model,
|
||||
signal: this._compactionAbortController.signal,
|
||||
})) as SessionEventResult | undefined;
|
||||
})) as SessionBeforeCompactResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
throw new Error("Compaction cancelled");
|
||||
|
|
@ -971,8 +970,7 @@ export class AgentSession {
|
|||
|
||||
if (this._hookRunner && savedCompactionEntry) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "compact",
|
||||
type: "session_compact",
|
||||
compactionEntry: savedCompactionEntry,
|
||||
fromHook,
|
||||
});
|
||||
|
|
@ -985,7 +983,7 @@ export class AgentSession {
|
|||
details,
|
||||
};
|
||||
} finally {
|
||||
this._compactionAbortController = null;
|
||||
this._compactionAbortController = undefined;
|
||||
this._reconnectToAgent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1051,13 +1049,13 @@ export class AgentSession {
|
|||
|
||||
try {
|
||||
if (!this.model) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
||||
if (!apiKey) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1065,29 +1063,28 @@ export class AgentSession {
|
|||
|
||||
const preparation = prepareCompaction(entries, settings);
|
||||
if (!preparation) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
let hookCompaction: CompactionResult | undefined;
|
||||
let fromHook = false;
|
||||
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
if (this._hookRunner?.hasHandlers("session_before_compact")) {
|
||||
// Get previous compactions, newest first
|
||||
const previousCompactions = entries.filter((e): e is CompactionEntry => e.type === "compaction").reverse();
|
||||
|
||||
const hookResult = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_compact",
|
||||
type: "session_before_compact",
|
||||
preparation,
|
||||
previousCompactions,
|
||||
customInstructions: undefined,
|
||||
model: this.model,
|
||||
signal: this._autoCompactionAbortController.signal,
|
||||
})) as SessionEventResult | undefined;
|
||||
})) as SessionBeforeCompactResult | undefined;
|
||||
|
||||
if (hookResult?.cancel) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1124,7 +1121,7 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
if (this._autoCompactionAbortController.signal.aborted) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1140,8 +1137,7 @@ export class AgentSession {
|
|||
|
||||
if (this._hookRunner && savedCompactionEntry) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "compact",
|
||||
type: "session_compact",
|
||||
compactionEntry: savedCompactionEntry,
|
||||
fromHook,
|
||||
});
|
||||
|
|
@ -1167,7 +1163,7 @@ export class AgentSession {
|
|||
}, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
|
||||
if (reason === "overflow") {
|
||||
throw new Error(
|
||||
|
|
@ -1175,7 +1171,7 @@ export class AgentSession {
|
|||
);
|
||||
}
|
||||
} finally {
|
||||
this._autoCompactionAbortController = null;
|
||||
this._autoCompactionAbortController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1267,7 +1263,7 @@ export class AgentSession {
|
|||
// Aborted during sleep - emit end event so UI can clean up
|
||||
const attempt = this._retryAttempt;
|
||||
this._retryAttempt = 0;
|
||||
this._retryAbortController = null;
|
||||
this._retryAbortController = undefined;
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
|
|
@ -1277,7 +1273,7 @@ export class AgentSession {
|
|||
this._resolveRetry();
|
||||
return false;
|
||||
}
|
||||
this._retryAbortController = null;
|
||||
this._retryAbortController = undefined;
|
||||
|
||||
// Retry via continue() - use setTimeout to break out of event handler chain
|
||||
setTimeout(() => {
|
||||
|
|
@ -1329,7 +1325,7 @@ export class AgentSession {
|
|||
|
||||
/** Whether auto-retry is currently in progress */
|
||||
get isRetrying(): boolean {
|
||||
return this._retryPromise !== null;
|
||||
return this._retryPromise !== undefined;
|
||||
}
|
||||
|
||||
/** Whether auto-retry is enabled */
|
||||
|
|
@ -1389,7 +1385,7 @@ export class AgentSession {
|
|||
|
||||
return result;
|
||||
} finally {
|
||||
this._bashAbortController = null;
|
||||
this._bashAbortController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1402,7 +1398,7 @@ export class AgentSession {
|
|||
|
||||
/** Whether a bash command is currently running */
|
||||
get isBashRunning(): boolean {
|
||||
return this._bashAbortController !== null;
|
||||
return this._bashAbortController !== undefined;
|
||||
}
|
||||
|
||||
/** Whether there are pending bash messages waiting to be flushed */
|
||||
|
|
@ -1439,15 +1435,14 @@ export class AgentSession {
|
|||
* @returns true if switch completed, false if cancelled by hook
|
||||
*/
|
||||
async switchSession(sessionPath: string): Promise<boolean> {
|
||||
const previousSessionFile = this.sessionFile;
|
||||
const previousSessionFile = this.sessionManager.getSessionFile();
|
||||
|
||||
// Emit before_switch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
// Emit session_before_switch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_switch")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_switch",
|
||||
type: "session_before_switch",
|
||||
targetSessionFile: sessionPath,
|
||||
})) as SessionEventResult | undefined;
|
||||
})) as SessionBeforeSwitchResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
return false;
|
||||
|
|
@ -1464,11 +1459,10 @@ export class AgentSession {
|
|||
// Reload messages
|
||||
const sessionContext = this.sessionManager.buildSessionContext();
|
||||
|
||||
// Emit session event to hooks
|
||||
// Emit session_switch event to hooks
|
||||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "switch",
|
||||
type: "session_switch",
|
||||
previousSessionFile,
|
||||
});
|
||||
}
|
||||
|
|
@ -1520,13 +1514,12 @@ export class AgentSession {
|
|||
|
||||
let skipConversationRestore = false;
|
||||
|
||||
// Emit before_branch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
// Emit session_before_branch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_branch")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_branch",
|
||||
targetTurnIndex: entryIndex,
|
||||
})) as SessionEventResult | undefined;
|
||||
type: "session_before_branch",
|
||||
entryIndex: entryIndex,
|
||||
})) as SessionBeforeBranchResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
return { selectedText, cancelled: true };
|
||||
|
|
@ -1534,27 +1527,20 @@ export class AgentSession {
|
|||
skipConversationRestore = result?.skipConversationRestore ?? false;
|
||||
}
|
||||
|
||||
// Create branched session ending before the selected message (returns null in --no-session mode)
|
||||
// User will re-enter/edit the selected message
|
||||
if (!selectedEntry.parentId) {
|
||||
throw new Error("Cannot branch from first message");
|
||||
}
|
||||
const newSessionFile = this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
||||
|
||||
// Update session file if we have one (file-based mode)
|
||||
if (newSessionFile !== null) {
|
||||
this.sessionManager.setSessionFile(newSessionFile);
|
||||
this.sessionManager.newSession();
|
||||
} else {
|
||||
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
||||
}
|
||||
|
||||
// Reload messages from entries (works for both file and in-memory mode)
|
||||
const sessionContext = this.sessionManager.buildSessionContext();
|
||||
|
||||
// Emit branch event to hooks (after branch completes)
|
||||
// Emit session_branch event to hooks (after branch completes)
|
||||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "branch",
|
||||
targetTurnIndex: entryIndex,
|
||||
type: "session_branch",
|
||||
previousSessionFile,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1664,9 +1650,9 @@ export class AgentSession {
|
|||
/**
|
||||
* Get text content of last assistant message.
|
||||
* Useful for /copy command.
|
||||
* @returns Text content, or null if no assistant message exists
|
||||
* @returns Text content, or undefined if no assistant message exists
|
||||
*/
|
||||
getLastAssistantText(): string | null {
|
||||
getLastAssistantText(): string | undefined {
|
||||
const lastAssistant = this.messages
|
||||
.slice()
|
||||
.reverse()
|
||||
|
|
@ -1678,7 +1664,7 @@ export class AgentSession {
|
|||
return true;
|
||||
});
|
||||
|
||||
if (!lastAssistant) return null;
|
||||
if (!lastAssistant) return undefined;
|
||||
|
||||
let text = "";
|
||||
for (const content of (lastAssistant as AssistantMessage).content) {
|
||||
|
|
@ -1687,7 +1673,7 @@ export class AgentSession {
|
|||
}
|
||||
}
|
||||
|
||||
return text.trim() || null;
|
||||
return text.trim() || undefined;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
@ -1704,7 +1690,7 @@ export class AgentSession {
|
|||
/**
|
||||
* Get the hook runner (for setting UI context and error handlers).
|
||||
*/
|
||||
get hookRunner(): HookRunner | null {
|
||||
get hookRunner(): HookRunner | undefined {
|
||||
return this._hookRunner;
|
||||
}
|
||||
|
||||
|
|
@ -1721,7 +1707,7 @@ export class AgentSession {
|
|||
*/
|
||||
private async _emitToolSessionEvent(
|
||||
reason: ToolSessionEvent["reason"],
|
||||
previousSessionFile: string | null,
|
||||
previousSessionFile: string | undefined,
|
||||
): Promise<void> {
|
||||
const event: ToolSessionEvent = {
|
||||
entries: this.sessionManager.getEntries(),
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ export class AuthStorage {
|
|||
/**
|
||||
* Get credential for a provider.
|
||||
*/
|
||||
get(provider: string): AuthCredential | null {
|
||||
return this.data[provider] ?? null;
|
||||
get(provider: string): AuthCredential | undefined {
|
||||
return this.data[provider] ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -191,7 +191,7 @@ export class AuthStorage {
|
|||
* 4. Environment variable
|
||||
* 5. Fallback resolver (models.json custom providers)
|
||||
*/
|
||||
async getApiKey(provider: string): Promise<string | null> {
|
||||
async getApiKey(provider: string): Promise<string | undefined> {
|
||||
// Runtime override takes highest priority
|
||||
const runtimeKey = this.runtimeOverrides.get(provider);
|
||||
if (runtimeKey) {
|
||||
|
|
@ -230,6 +230,6 @@ export class AuthStorage {
|
|||
if (envKey) return envKey;
|
||||
|
||||
// Fall back to custom resolver (e.g., models.json custom providers)
|
||||
return this.fallbackResolver?.(provider) ?? null;
|
||||
return this.fallbackResolver?.(provider) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ export interface BashExecutorOptions {
|
|||
export interface BashResult {
|
||||
/** Combined stdout + stderr output (sanitized, possibly truncated) */
|
||||
output: string;
|
||||
/** Process exit code (null if killed/cancelled) */
|
||||
exitCode: number | null;
|
||||
/** Process exit code (undefined if killed/cancelled) */
|
||||
exitCode: number | undefined;
|
||||
/** Whether the command was cancelled via signal */
|
||||
cancelled: boolean;
|
||||
/** Whether the output was truncated */
|
||||
|
|
@ -88,7 +88,7 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|||
child.kill();
|
||||
resolve({
|
||||
output: "",
|
||||
exitCode: null,
|
||||
exitCode: undefined,
|
||||
cancelled: true,
|
||||
truncated: false,
|
||||
});
|
||||
|
|
@ -154,7 +154,7 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|||
|
||||
resolve({
|
||||
output: truncationResult.truncated ? truncationResult.content : fullOutput,
|
||||
exitCode: code,
|
||||
exitCode: cancelled ? undefined : code,
|
||||
cancelled,
|
||||
truncated: truncationResult.truncated,
|
||||
fullOutputPath: tempFilePath,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import type { CompactionEntry, SessionEntry } from "./session-manager.js";
|
|||
|
||||
/**
|
||||
* Extract AgentMessage from an entry if it produces one.
|
||||
* Returns null for entries that don't contribute to LLM context.
|
||||
* Returns undefined for entries that don't contribute to LLM context.
|
||||
*/
|
||||
function getMessageFromEntry(entry: SessionEntry): AgentMessage | null {
|
||||
function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
||||
if (entry.type === "message") {
|
||||
return entry.message;
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | null {
|
|||
if (entry.type === "branch_summary") {
|
||||
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Result from compact() - SessionManager adds uuid/parentUuid when saving */
|
||||
|
|
@ -69,20 +69,20 @@ export function calculateContextTokens(usage: Usage): number {
|
|||
* Get usage from an assistant message if available.
|
||||
* Skips aborted and error messages as they don't have valid usage data.
|
||||
*/
|
||||
function getAssistantUsage(msg: AgentMessage): Usage | null {
|
||||
function getAssistantUsage(msg: AgentMessage): Usage | undefined {
|
||||
if (msg.role === "assistant" && "usage" in msg) {
|
||||
const assistantMsg = msg as AssistantMessage;
|
||||
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
||||
return assistantMsg.usage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the last non-aborted assistant message usage from session entries.
|
||||
*/
|
||||
export function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {
|
||||
export function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const entry = entries[i];
|
||||
if (entry.type === "message") {
|
||||
|
|
@ -90,7 +90,7 @@ export function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {
|
|||
if (usage) return usage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -398,9 +398,12 @@ export interface CompactionPreparation {
|
|||
boundaryStart: number;
|
||||
}
|
||||
|
||||
export function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {
|
||||
export function prepareCompaction(
|
||||
entries: SessionEntry[],
|
||||
settings: CompactionSettings,
|
||||
): CompactionPreparation | undefined {
|
||||
if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let prevCompactionIndex = -1;
|
||||
|
|
@ -421,7 +424,7 @@ export function prepareCompaction(entries: SessionEntry[], settings: CompactionS
|
|||
// Get UUID of first kept entry
|
||||
const firstKeptEntry = entries[cutPoint.firstKeptEntryIndex];
|
||||
if (!firstKeptEntry?.id) {
|
||||
return null; // Session needs migration
|
||||
return undefined; // Session needs migration
|
||||
}
|
||||
const firstKeptEntryId = firstKeptEntry.id;
|
||||
|
||||
|
|
|
|||
|
|
@ -86,9 +86,9 @@ function resolveToolPath(toolPath: string, cwd: string): string {
|
|||
*/
|
||||
function createNoOpUIContext(): HookUIContext {
|
||||
return {
|
||||
select: async () => null,
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => null,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
custom: () => ({ close: () => {}, requestRender: () => {} }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ export interface ToolAPI {
|
|||
export interface SessionEvent {
|
||||
/** All session entries (including pre-compaction history) */
|
||||
entries: SessionEntry[];
|
||||
/** Current session file path, or null in --no-session mode */
|
||||
sessionFile: string | null;
|
||||
/** Previous session file path, or null for "start" and "new" */
|
||||
previousSessionFile: string | null;
|
||||
/** Current session file path, or undefined in --no-session mode */
|
||||
sessionFile: string | undefined;
|
||||
/** Previous session file path, or undefined for "start" and "new" */
|
||||
previousSessionFile: string | undefined;
|
||||
/** Reason for the session event */
|
||||
reason: "start" | "switch" | "branch" | "new";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ function resolveColorValue(
|
|||
}
|
||||
|
||||
/** Load theme JSON from built-in or custom themes directory. */
|
||||
function loadThemeJson(name: string): ThemeJson | null {
|
||||
function loadThemeJson(name: string): ThemeJson | undefined {
|
||||
// Try built-in themes first
|
||||
const themesDir = getThemesDir();
|
||||
const builtinPath = path.join(themesDir, `${name}.json`);
|
||||
|
|
@ -129,7 +129,7 @@ function loadThemeJson(name: string): ThemeJson | null {
|
|||
try {
|
||||
return JSON.parse(readFileSync(builtinPath, "utf-8")) as ThemeJson;
|
||||
} catch {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,11 +140,11 @@ function loadThemeJson(name: string): ThemeJson | null {
|
|||
try {
|
||||
return JSON.parse(readFileSync(customPath, "utf-8")) as ThemeJson;
|
||||
} catch {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Build complete theme colors object, resolving theme JSON values against defaults. */
|
||||
|
|
@ -831,7 +831,9 @@ function formatMessage(
|
|||
|
||||
switch (message.role) {
|
||||
case "bashExecution": {
|
||||
const isError = message.cancelled || (message.exitCode !== 0 && message.exitCode !== null);
|
||||
const isError =
|
||||
message.cancelled ||
|
||||
(message.exitCode !== 0 && message.exitCode !== null && message.exitCode !== undefined);
|
||||
|
||||
html += `<div class="tool-execution user-bash${isError ? " user-bash-error" : ""}">`;
|
||||
html += timestampHtml;
|
||||
|
|
@ -844,7 +846,7 @@ function formatMessage(
|
|||
|
||||
if (message.cancelled) {
|
||||
html += `<div class="bash-status warning">(cancelled)</div>`;
|
||||
} else if (message.exitCode !== 0 && message.exitCode !== null) {
|
||||
} else if (message.exitCode !== 0 && message.exitCode !== null && message.exitCode !== undefined) {
|
||||
html += `<div class="bash-status error">(exit ${message.exitCode})</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -1020,7 +1022,7 @@ function generateHtml(data: ParsedSessionData, filename: string, colors: ThemeCo
|
|||
const lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;
|
||||
|
||||
const contextWindow = data.contextWindow || 0;
|
||||
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : null;
|
||||
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : undefined;
|
||||
|
||||
let messagesHtml = "";
|
||||
for (const event of data.sessionEvents) {
|
||||
|
|
|
|||
|
|
@ -9,49 +9,4 @@ export {
|
|||
} from "./loader.js";
|
||||
export { execCommand, HookRunner, type HookErrorListener } from "./runner.js";
|
||||
export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
|
||||
export type {
|
||||
AgentEndEvent,
|
||||
AgentStartEvent,
|
||||
BeforeAgentStartEvent,
|
||||
BeforeAgentStartEventResult,
|
||||
BashToolResultEvent,
|
||||
ContextEvent,
|
||||
ContextEventResult,
|
||||
CustomToolResultEvent,
|
||||
EditToolResultEvent,
|
||||
ExecOptions,
|
||||
ExecResult,
|
||||
FindToolResultEvent,
|
||||
GrepToolResultEvent,
|
||||
HookAPI,
|
||||
HookCommandContext,
|
||||
HookError,
|
||||
HookEvent,
|
||||
HookEventContext,
|
||||
HookFactory,
|
||||
HookMessageRenderer,
|
||||
HookMessageRenderOptions,
|
||||
HookUIContext,
|
||||
LsToolResultEvent,
|
||||
ReadonlySessionManager,
|
||||
ReadToolResultEvent,
|
||||
RegisteredCommand,
|
||||
SessionEvent,
|
||||
SessionEventResult,
|
||||
ToolCallEvent,
|
||||
ToolCallEventResult,
|
||||
ToolResultEvent,
|
||||
ToolResultEventResult,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
WriteToolResultEvent,
|
||||
} from "./types.js";
|
||||
export {
|
||||
isBashToolResult,
|
||||
isEditToolResult,
|
||||
isFindToolResult,
|
||||
isGrepToolResult,
|
||||
isLsToolResult,
|
||||
isReadToolResult,
|
||||
isWriteToolResult,
|
||||
} from "./types.js";
|
||||
export type * from "./types.js";
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ import type {
|
|||
HookMessageRenderer,
|
||||
HookUIContext,
|
||||
RegisteredCommand,
|
||||
SessionEvent,
|
||||
SessionEventResult,
|
||||
SessionBeforeCompactResult,
|
||||
ToolCallEvent,
|
||||
ToolCallEventResult,
|
||||
ToolResultEventResult,
|
||||
|
|
@ -53,9 +52,9 @@ function createTimeout(ms: number): { promise: Promise<never>; clear: () => void
|
|||
|
||||
/** No-op UI context used when no UI is available */
|
||||
const noOpUIContext: HookUIContext = {
|
||||
select: async () => null,
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => null,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
custom: () => ({ close: () => {}, requestRender: () => {} }),
|
||||
};
|
||||
|
|
@ -228,12 +227,26 @@ export class HookRunner {
|
|||
}
|
||||
|
||||
/**
|
||||
* Emit an event to all hooks.
|
||||
* Returns the result from session/tool_result events (if any handler returns one).
|
||||
* Check if event type is a session "before_*" event that can be cancelled.
|
||||
*/
|
||||
async emit(event: HookEvent): Promise<SessionEventResult | ToolResultEventResult | undefined> {
|
||||
private isSessionBeforeEvent(
|
||||
type: string,
|
||||
): type is "session_before_switch" | "session_before_new" | "session_before_branch" | "session_before_compact" {
|
||||
return (
|
||||
type === "session_before_switch" ||
|
||||
type === "session_before_new" ||
|
||||
type === "session_before_branch" ||
|
||||
type === "session_before_compact"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to all hooks.
|
||||
* Returns the result from session before_* / tool_result events (if any handler returns one).
|
||||
*/
|
||||
async emit(event: HookEvent): Promise<SessionBeforeCompactResult | ToolResultEventResult | undefined> {
|
||||
const ctx = this.createContext();
|
||||
let result: SessionEventResult | ToolResultEventResult | undefined;
|
||||
let result: SessionBeforeCompactResult | ToolResultEventResult | undefined;
|
||||
|
||||
for (const hook of this.hooks) {
|
||||
const handlers = hook.handlers.get(event.type);
|
||||
|
|
@ -241,11 +254,10 @@ export class HookRunner {
|
|||
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
// No timeout for before_compact events (like tool_call, they may take a while)
|
||||
const isBeforeCompact = event.type === "session" && (event as SessionEvent).reason === "before_compact";
|
||||
// No timeout for session_before_compact events (like tool_call, they may take a while)
|
||||
let handlerResult: unknown;
|
||||
|
||||
if (isBeforeCompact) {
|
||||
if (event.type === "session_before_compact") {
|
||||
handlerResult = await handler(event, ctx);
|
||||
} else {
|
||||
const timeout = createTimeout(this.timeout);
|
||||
|
|
@ -253,9 +265,9 @@ export class HookRunner {
|
|||
timeout.clear();
|
||||
}
|
||||
|
||||
// For session events, capture the result (for before_* cancellation)
|
||||
if (event.type === "session" && handlerResult) {
|
||||
result = handlerResult as SessionEventResult;
|
||||
// For session before_* events, capture the result (for cancellation)
|
||||
if (this.isSessionBeforeEvent(event.type) && handlerResult) {
|
||||
result = handlerResult as SessionBeforeCompactResult;
|
||||
// If cancelled, stop processing further hooks
|
||||
if (result.cancel) {
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -13,13 +13,7 @@ import type { CompactionPreparation, CompactionResult } from "../compaction.js";
|
|||
import type { ExecOptions, ExecResult } from "../exec.js";
|
||||
import type { HookMessage } from "../messages.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
import type {
|
||||
CompactionEntry,
|
||||
SessionEntry,
|
||||
SessionHeader,
|
||||
SessionManager,
|
||||
SessionTreeNode,
|
||||
} from "../session-manager.js";
|
||||
import type { CompactionEntry, SessionManager } from "../session-manager.js";
|
||||
|
||||
/**
|
||||
* Read-only view of SessionManager for hooks.
|
||||
|
|
@ -64,7 +58,7 @@ export interface HookUIContext {
|
|||
* @param options - Array of string options
|
||||
* @returns Selected option string, or null if cancelled
|
||||
*/
|
||||
select(title: string, options: string[]): Promise<string | null>;
|
||||
select(title: string, options: string[]): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Show a confirmation dialog.
|
||||
|
|
@ -74,9 +68,9 @@ export interface HookUIContext {
|
|||
|
||||
/**
|
||||
* Show a text input dialog.
|
||||
* @returns User input, or null if cancelled
|
||||
* @returns User input, or undefined if cancelled
|
||||
*/
|
||||
input(title: string, placeholder?: string): Promise<string | null>;
|
||||
input(title: string, placeholder?: string): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Show a notification to the user.
|
||||
|
|
@ -110,69 +104,91 @@ export interface HookEventContext {
|
|||
}
|
||||
|
||||
// ============================================================================
|
||||
// Events
|
||||
// Session Events
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Base fields shared by all session events.
|
||||
*/
|
||||
interface SessionEventBase {
|
||||
type: "session";
|
||||
/** Fired on initial session load */
|
||||
export interface SessionStartEvent {
|
||||
type: "session_start";
|
||||
}
|
||||
|
||||
/**
|
||||
* Event data for session events.
|
||||
* Discriminated union based on reason.
|
||||
*
|
||||
* Lifecycle:
|
||||
* - start: Initial session load
|
||||
* - before_switch / switch: Session switch (e.g., /resume command)
|
||||
* - before_new / new: New session (e.g., /new command)
|
||||
* - before_branch / branch: Session branch (e.g., /branch command)
|
||||
* - before_compact / compact: Before/after context compaction
|
||||
* - shutdown: Process exit (SIGINT/SIGTERM)
|
||||
*
|
||||
* "before_*" events fire before the action and can be cancelled via SessionEventResult.
|
||||
* Other events fire after the action completes.
|
||||
*/
|
||||
/** Fired before switching to another session (can be cancelled) */
|
||||
export interface SessionBeforeSwitchEvent {
|
||||
type: "session_before_switch";
|
||||
/** Session file we're switching to */
|
||||
targetSessionFile: string;
|
||||
}
|
||||
|
||||
/** Fired after switching to another session */
|
||||
export interface SessionSwitchEvent {
|
||||
type: "session_switch";
|
||||
/** Session file we came from */
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
|
||||
/** Fired before creating a new session (can be cancelled) */
|
||||
export interface SessionBeforeNewEvent {
|
||||
type: "session_before_new";
|
||||
}
|
||||
|
||||
/** Fired after creating a new session */
|
||||
export interface SessionNewEvent {
|
||||
type: "session_new";
|
||||
}
|
||||
|
||||
/** Fired before branching a session (can be cancelled) */
|
||||
export interface SessionBeforeBranchEvent {
|
||||
type: "session_before_branch";
|
||||
/** Index of the entry in the session (SessionManager.getEntries()) to branch from */
|
||||
entryIndex: number;
|
||||
}
|
||||
|
||||
/** Fired after branching a session */
|
||||
export interface SessionBranchEvent {
|
||||
type: "session_branch";
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
|
||||
/** Fired before context compaction (can be cancelled or customized) */
|
||||
export interface SessionBeforeCompactEvent {
|
||||
type: "session_before_compact";
|
||||
/** Compaction preparation with cut point, messages to summarize/keep, etc. */
|
||||
preparation: CompactionPreparation;
|
||||
/** Previous compaction entries, newest first. Use for iterative summarization. */
|
||||
previousCompactions: CompactionEntry[];
|
||||
/** Optional user-provided instructions for the summary */
|
||||
customInstructions?: string;
|
||||
/** Current model */
|
||||
model: Model<any>;
|
||||
/** Abort signal - hooks should pass this to LLM calls and check it periodically */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
/** Fired after context compaction */
|
||||
export interface SessionCompactEvent {
|
||||
type: "session_compact";
|
||||
compactionEntry: CompactionEntry;
|
||||
/** Whether the compaction entry was provided by a hook */
|
||||
fromHook: boolean;
|
||||
}
|
||||
|
||||
/** Fired on process exit (SIGINT/SIGTERM) */
|
||||
export interface SessionShutdownEvent {
|
||||
type: "session_shutdown";
|
||||
}
|
||||
|
||||
/** Union of all session event types */
|
||||
export type SessionEvent =
|
||||
| (SessionEventBase & {
|
||||
reason: "start" | "new" | "before_new" | "shutdown";
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "before_switch";
|
||||
/** Session file we're switching to */
|
||||
targetSessionFile: string;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "switch";
|
||||
/** Session file we came from */
|
||||
previousSessionFile: string | null;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "branch" | "before_branch";
|
||||
/** Index of the turn to branch from */
|
||||
targetTurnIndex: number;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "before_compact";
|
||||
/** Compaction preparation with cut point, messages to summarize/keep, etc. */
|
||||
preparation: CompactionPreparation;
|
||||
/** Previous compaction entries, newest first. Use for iterative summarization. */
|
||||
previousCompactions: CompactionEntry[];
|
||||
/** Optional user-provided instructions for the summary */
|
||||
customInstructions?: string;
|
||||
/** Current model */
|
||||
model: Model<any>;
|
||||
/** Abort signal - hooks should pass this to LLM calls and check it periodically */
|
||||
signal: AbortSignal;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "compact";
|
||||
compactionEntry: CompactionEntry;
|
||||
/** Whether the compaction entry was provided by a hook */
|
||||
fromHook: boolean;
|
||||
});
|
||||
| SessionStartEvent
|
||||
| SessionBeforeSwitchEvent
|
||||
| SessionSwitchEvent
|
||||
| SessionBeforeNewEvent
|
||||
| SessionNewEvent
|
||||
| SessionBeforeBranchEvent
|
||||
| SessionBranchEvent
|
||||
| SessionBeforeCompactEvent
|
||||
| SessionCompactEvent
|
||||
| SessionShutdownEvent;
|
||||
|
||||
/**
|
||||
* Event data for context event.
|
||||
|
|
@ -408,16 +424,45 @@ export interface BeforeAgentStartEventResult {
|
|||
message?: Pick<HookMessage, "customType" | "content" | "display" | "details">;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type for session event handlers.
|
||||
* Allows hooks to cancel "before_*" actions.
|
||||
*/
|
||||
export interface SessionEventResult {
|
||||
/** If true, cancel the pending action (switch, clear, or branch) */
|
||||
/** Return type for session_before_switch handlers */
|
||||
export interface SessionBeforeSwitchResult {
|
||||
/** If true, cancel the switch */
|
||||
cancel?: boolean;
|
||||
/** If true (for before_branch only), skip restoring conversation to branch point while still creating the branched session file */
|
||||
}
|
||||
|
||||
/** Return type for session_before_new handlers */
|
||||
export interface SessionBeforeNewResult {
|
||||
/** If true, cancel the new session */
|
||||
cancel?: boolean;
|
||||
}
|
||||
|
||||
/** Return type for session_before_branch handlers */
|
||||
export interface SessionBeforeBranchResult {
|
||||
/**
|
||||
* If true, abort the branch entirely. No new session file is created,
|
||||
* conversation stays unchanged.
|
||||
*/
|
||||
cancel?: boolean;
|
||||
/**
|
||||
* If true, the branch proceeds (new session file created, session state updated)
|
||||
* but the in-memory conversation is NOT rewound to the branch point.
|
||||
*
|
||||
* Use case: git-checkpoint hook that restores code state separately.
|
||||
* The hook handles state restoration itself, so it doesn't want the
|
||||
* agent's conversation to be rewound (which would lose recent context).
|
||||
*
|
||||
* - `cancel: true` → nothing happens, user stays in current session
|
||||
* - `skipConversationRestore: true` → branch happens, but messages stay as-is
|
||||
* - neither → branch happens AND messages rewind to branch point (default)
|
||||
*/
|
||||
skipConversationRestore?: boolean;
|
||||
/** Custom compaction result (for before_compact event) - SessionManager adds id/parentId */
|
||||
}
|
||||
|
||||
/** Return type for session_before_compact handlers */
|
||||
export interface SessionBeforeCompactResult {
|
||||
/** If true, cancel the compaction */
|
||||
cancel?: boolean;
|
||||
/** Custom compaction result - SessionManager adds id/parentId */
|
||||
compaction?: CompactionResult;
|
||||
}
|
||||
|
||||
|
|
@ -427,8 +472,10 @@ export interface SessionEventResult {
|
|||
|
||||
/**
|
||||
* Handler function type for each event.
|
||||
* Handlers can return R, undefined, or void (bare return statements).
|
||||
*/
|
||||
export type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements in handlers
|
||||
export type HookHandler<E, R = undefined> = (event: E, ctx: HookEventContext) => Promise<R | void> | R | void;
|
||||
|
||||
export interface HookMessageRenderOptions {
|
||||
/** Whether the view is expanded */
|
||||
|
|
@ -443,7 +490,7 @@ export type HookMessageRenderer<T = unknown> = (
|
|||
message: HookMessage<T>,
|
||||
options: HookMessageRenderOptions,
|
||||
theme: Theme,
|
||||
) => Component | null;
|
||||
) => Component | undefined;
|
||||
|
||||
/**
|
||||
* Context passed to hook command handlers.
|
||||
|
|
@ -478,21 +525,30 @@ export interface RegisteredCommand {
|
|||
* Hooks use pi.on() to subscribe to events and pi.sendMessage() to inject messages.
|
||||
*/
|
||||
export interface HookAPI {
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||
on(event: "session", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||
on(event: "context", handler: HookHandler<ContextEvent, ContextEventResult | void>): void;
|
||||
// Session events
|
||||
on(event: "session_start", handler: HookHandler<SessionStartEvent>): void;
|
||||
on(event: "session_before_switch", handler: HookHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>): void;
|
||||
on(event: "session_switch", handler: HookHandler<SessionSwitchEvent>): void;
|
||||
on(event: "session_before_new", handler: HookHandler<SessionBeforeNewEvent, SessionBeforeNewResult>): void;
|
||||
on(event: "session_new", handler: HookHandler<SessionNewEvent>): void;
|
||||
on(event: "session_before_branch", handler: HookHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>): void;
|
||||
on(event: "session_branch", handler: HookHandler<SessionBranchEvent>): void;
|
||||
on(
|
||||
event: "before_agent_start",
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||
handler: HookHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult | void>,
|
||||
event: "session_before_compact",
|
||||
handler: HookHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
|
||||
): void;
|
||||
on(event: "session_compact", handler: HookHandler<SessionCompactEvent>): void;
|
||||
on(event: "session_shutdown", handler: HookHandler<SessionShutdownEvent>): void;
|
||||
|
||||
// Context and agent events
|
||||
on(event: "context", handler: HookHandler<ContextEvent, ContextEventResult>): void;
|
||||
on(event: "before_agent_start", handler: HookHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
|
||||
on(event: "agent_start", handler: HookHandler<AgentStartEvent>): void;
|
||||
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
|
||||
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
|
||||
on(event: "turn_end", handler: HookHandler<TurnEndEvent>): void;
|
||||
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;
|
||||
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;
|
||||
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult>): void;
|
||||
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult>): void;
|
||||
|
||||
/**
|
||||
* Send a custom message to the session. Creates a CustomMessageEntry that
|
||||
|
|
@ -545,7 +601,7 @@ export interface HookAPI {
|
|||
/**
|
||||
* Register a custom renderer for CustomMessageEntry with a specific customType.
|
||||
* The renderer is called when rendering the entry in the TUI.
|
||||
* Return null to use the default renderer.
|
||||
* Return nothing to use the default renderer.
|
||||
*/
|
||||
registerMessageRenderer<T = unknown>(customType: string, renderer: HookMessageRenderer<T>): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export interface BashExecutionMessage {
|
|||
role: "bashExecution";
|
||||
command: string;
|
||||
output: string;
|
||||
exitCode: number | null;
|
||||
exitCode: number | undefined;
|
||||
cancelled: boolean;
|
||||
truncated: boolean;
|
||||
fullOutputPath?: string;
|
||||
|
|
@ -86,7 +86,7 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
|
|||
}
|
||||
if (msg.cancelled) {
|
||||
text += "\n\n(command cancelled)";
|
||||
} else if (msg.exitCode !== null && msg.exitCode !== 0) {
|
||||
} else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
|
||||
text += `\n\nCommand exited with code ${msg.exitCode}`;
|
||||
}
|
||||
if (msg.truncated && msg.fullOutputPath) {
|
||||
|
|
@ -145,7 +145,7 @@ export function createHookMessage(
|
|||
*/
|
||||
export function convertToLlm(messages: AgentMessage[]): Message[] {
|
||||
return messages
|
||||
.map((m): Message | null => {
|
||||
.map((m): Message | undefined => {
|
||||
switch (m.role) {
|
||||
case "bashExecution":
|
||||
return {
|
||||
|
|
@ -182,8 +182,8 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|||
default:
|
||||
// biome-ignore lint/correctness/noSwitchDeclarations: fine
|
||||
const _exhaustiveCheck: never = m;
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter((m) => m !== null);
|
||||
.filter((m) => m !== undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,11 +90,11 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|||
export class ModelRegistry {
|
||||
private models: Model<Api>[] = [];
|
||||
private customProviderApiKeys: Map<string, string> = new Map();
|
||||
private loadError: string | null = null;
|
||||
private loadError: string | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
readonly authStorage: AuthStorage,
|
||||
private modelsJsonPath: string | null = null,
|
||||
private modelsJsonPath: string | undefined = undefined,
|
||||
) {
|
||||
// Set up fallback resolver for custom provider API keys
|
||||
this.authStorage.setFallbackResolver((provider) => {
|
||||
|
|
@ -114,14 +114,14 @@ export class ModelRegistry {
|
|||
*/
|
||||
refresh(): void {
|
||||
this.customProviderApiKeys.clear();
|
||||
this.loadError = null;
|
||||
this.loadError = undefined;
|
||||
this.loadModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any error from loading models.json (null if no error).
|
||||
* Get any error from loading models.json (undefined if no error).
|
||||
*/
|
||||
getError(): string | null {
|
||||
getError(): string | undefined {
|
||||
return this.loadError;
|
||||
}
|
||||
|
||||
|
|
@ -160,9 +160,9 @@ export class ModelRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
private loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | null } {
|
||||
private loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | undefined } {
|
||||
if (!existsSync(modelsJsonPath)) {
|
||||
return { models: [], error: null };
|
||||
return { models: [], error: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -186,7 +186,7 @@ export class ModelRegistry {
|
|||
this.validateConfig(config);
|
||||
|
||||
// Parse models
|
||||
return { models: this.parseModels(config), error: null };
|
||||
return { models: this.parseModels(config), error: undefined };
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return {
|
||||
|
|
@ -294,14 +294,14 @@ export class ModelRegistry {
|
|||
/**
|
||||
* Find a model by provider and ID.
|
||||
*/
|
||||
find(provider: string, modelId: string): Model<Api> | null {
|
||||
return this.models.find((m) => m.provider === provider && m.id === modelId) ?? null;
|
||||
find(provider: string, modelId: string): Model<Api> | undefined {
|
||||
return this.models.find((m) => m.provider === provider && m.id === modelId) ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key for a model.
|
||||
*/
|
||||
async getApiKey(model: Model<Api>): Promise<string | null> {
|
||||
async getApiKey(model: Model<Api>): Promise<string | undefined> {
|
||||
return this.authStorage.getApiKey(model.provider);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ function isAlias(id: string): boolean {
|
|||
|
||||
/**
|
||||
* Try to match a pattern to a model from the available models list.
|
||||
* Returns the matched model or null if no match found.
|
||||
* Returns the matched model or undefined if no match found.
|
||||
*/
|
||||
function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | null {
|
||||
function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
||||
// Check for provider/modelId format (provider is everything before the first /)
|
||||
const slashIndex = modelPattern.indexOf("/");
|
||||
if (slashIndex !== -1) {
|
||||
|
|
@ -75,7 +75,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|||
);
|
||||
|
||||
if (matches.length === 0) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Separate into aliases and dated versions
|
||||
|
|
@ -94,9 +94,9 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|||
}
|
||||
|
||||
export interface ParsedModelResult {
|
||||
model: Model<Api> | null;
|
||||
model: Model<Api> | undefined;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
warning: string | null;
|
||||
warning: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -116,14 +116,14 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
|
|||
// Try exact match first
|
||||
const exactMatch = tryMatchModel(pattern, availableModels);
|
||||
if (exactMatch) {
|
||||
return { model: exactMatch, thinkingLevel: "off", warning: null };
|
||||
return { model: exactMatch, thinkingLevel: "off", warning: undefined };
|
||||
}
|
||||
|
||||
// No match - try splitting on last colon if present
|
||||
const lastColonIndex = pattern.lastIndexOf(":");
|
||||
if (lastColonIndex === -1) {
|
||||
// No colons, pattern simply doesn't match any model
|
||||
return { model: null, thinkingLevel: "off", warning: null };
|
||||
return { model: undefined, thinkingLevel: "off", warning: undefined };
|
||||
}
|
||||
|
||||
const prefix = pattern.substring(0, lastColonIndex);
|
||||
|
|
@ -193,9 +193,9 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
|||
}
|
||||
|
||||
export interface InitialModelResult {
|
||||
model: Model<Api> | null;
|
||||
model: Model<Api> | undefined;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
fallbackMessage: string | null;
|
||||
fallbackMessage: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -227,7 +227,7 @@ export async function findInitialModel(options: {
|
|||
modelRegistry,
|
||||
} = options;
|
||||
|
||||
let model: Model<Api> | null = null;
|
||||
let model: Model<Api> | undefined;
|
||||
let thinkingLevel: ThinkingLevel = "off";
|
||||
|
||||
// 1. CLI args take priority
|
||||
|
|
@ -237,7 +237,7 @@ export async function findInitialModel(options: {
|
|||
console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
return { model: found, thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: found, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// 2. Use first model from scoped models (skip if continuing/resuming)
|
||||
|
|
@ -245,7 +245,7 @@ export async function findInitialModel(options: {
|
|||
return {
|
||||
model: scopedModels[0].model,
|
||||
thinkingLevel: scopedModels[0].thinkingLevel,
|
||||
fallbackMessage: null,
|
||||
fallbackMessage: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ export async function findInitialModel(options: {
|
|||
if (defaultThinkingLevel) {
|
||||
thinkingLevel = defaultThinkingLevel;
|
||||
}
|
||||
return { model, thinkingLevel, fallbackMessage: null };
|
||||
return { model, thinkingLevel, fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,16 +270,16 @@ export async function findInitialModel(options: {
|
|||
const defaultId = defaultModelPerProvider[provider];
|
||||
const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
|
||||
if (match) {
|
||||
return { model: match, thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: match, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
// If no default found, use first available
|
||||
return { model: availableModels[0], thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: availableModels[0], thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// 5. No model found
|
||||
return { model: null, thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: undefined, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -288,10 +288,10 @@ export async function findInitialModel(options: {
|
|||
export async function restoreModelFromSession(
|
||||
savedProvider: string,
|
||||
savedModelId: string,
|
||||
currentModel: Model<Api> | null,
|
||||
currentModel: Model<Api> | undefined,
|
||||
shouldPrintMessages: boolean,
|
||||
modelRegistry: ModelRegistry,
|
||||
): Promise<{ model: Model<Api> | null; fallbackMessage: string | null }> {
|
||||
): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
|
||||
const restoredModel = modelRegistry.find(savedProvider, savedModelId);
|
||||
|
||||
// Check if restored model exists and has a valid API key
|
||||
|
|
@ -301,7 +301,7 @@ export async function restoreModelFromSession(
|
|||
if (shouldPrintMessages) {
|
||||
console.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));
|
||||
}
|
||||
return { model: restoredModel, fallbackMessage: null };
|
||||
return { model: restoredModel, fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// Model not found or no API key - fall back
|
||||
|
|
@ -327,7 +327,7 @@ export async function restoreModelFromSession(
|
|||
|
||||
if (availableModels.length > 0) {
|
||||
// Try to find a default model from known providers
|
||||
let fallbackModel: Model<Api> | null = null;
|
||||
let fallbackModel: Model<Api> | undefined;
|
||||
for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
|
||||
const defaultId = defaultModelPerProvider[provider];
|
||||
const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
|
||||
|
|
@ -353,5 +353,5 @@ export async function restoreModelFromSession(
|
|||
}
|
||||
|
||||
// No models available
|
||||
return { model: null, fallbackMessage: null };
|
||||
return { model: undefined, fallbackMessage: undefined };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -530,7 +530,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
customToolsResult = result;
|
||||
}
|
||||
|
||||
let hookRunner: HookRunner | null = null;
|
||||
let hookRunner: HookRunner | undefined;
|
||||
if (options.hooks !== undefined) {
|
||||
if (options.hooks.length > 0) {
|
||||
const loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ export class SessionManager {
|
|||
private labelsById: Map<string, string> = new Map();
|
||||
private leafId: string = "";
|
||||
|
||||
private constructor(cwd: string, sessionDir: string, sessionFile: string | null, persist: boolean) {
|
||||
private constructor(cwd: string, sessionDir: string, sessionFile: string | undefined, persist: boolean) {
|
||||
this.cwd = cwd;
|
||||
this.sessionDir = sessionDir;
|
||||
this.persist = persist;
|
||||
|
|
@ -484,7 +484,7 @@ export class SessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
newSession(): void {
|
||||
newSession(): string | undefined {
|
||||
this.sessionId = randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const header: SessionHeader = {
|
||||
|
|
@ -503,6 +503,7 @@ export class SessionManager {
|
|||
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
||||
this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
||||
}
|
||||
return this.sessionFile;
|
||||
}
|
||||
|
||||
private _buildIndex(): void {
|
||||
|
|
@ -841,9 +842,9 @@ export class SessionManager {
|
|||
/**
|
||||
* Create a new session file containing only the path from root to the specified leaf.
|
||||
* Useful for extracting a single conversation path from a branched session.
|
||||
* Returns the new session file path, or null if not persisting.
|
||||
* Returns the new session file path, or undefined if not persisting.
|
||||
*/
|
||||
createBranchedSession(leafId: string): string | null {
|
||||
createBranchedSession(leafId: string): string | undefined {
|
||||
const path = this.getPath(leafId);
|
||||
if (path.length === 0) {
|
||||
throw new Error(`Entry ${leafId} not found`);
|
||||
|
|
@ -883,6 +884,7 @@ export class SessionManager {
|
|||
// Write fresh label entries at the end
|
||||
const lastEntryId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null;
|
||||
let parentId = lastEntryId;
|
||||
const labelEntries: LabelEntry[] = [];
|
||||
for (const { targetId, label } of labelsToWrite) {
|
||||
const labelEntry: LabelEntry = {
|
||||
type: "label",
|
||||
|
|
@ -894,8 +896,12 @@ export class SessionManager {
|
|||
};
|
||||
appendFileSync(newSessionFile, `${JSON.stringify(labelEntry)}\n`);
|
||||
pathEntryIds.add(labelEntry.id);
|
||||
labelEntries.push(labelEntry);
|
||||
parentId = labelEntry.id;
|
||||
}
|
||||
this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
||||
this.sessionId = newSessionId;
|
||||
this._buildIndex();
|
||||
return newSessionFile;
|
||||
}
|
||||
|
||||
|
|
@ -917,7 +923,7 @@ export class SessionManager {
|
|||
this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
||||
this.sessionId = newSessionId;
|
||||
this._buildIndex();
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -927,7 +933,7 @@ export class SessionManager {
|
|||
*/
|
||||
static create(cwd: string, sessionDir?: string): SessionManager {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
return new SessionManager(cwd, dir, null, true);
|
||||
return new SessionManager(cwd, dir, undefined, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -956,12 +962,12 @@ export class SessionManager {
|
|||
if (mostRecent) {
|
||||
return new SessionManager(cwd, dir, mostRecent, true);
|
||||
}
|
||||
return new SessionManager(cwd, dir, null, true);
|
||||
return new SessionManager(cwd, dir, undefined, true);
|
||||
}
|
||||
|
||||
/** Create an in-memory session (no file persistence) */
|
||||
static inMemory(cwd: string = process.cwd()): SessionManager {
|
||||
return new SessionManager(cwd, "", null, false);
|
||||
return new SessionManager(cwd, "", undefined, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue