feat(coding-agent): restored plan mode on resume from session

- Added ModeChangeEntry type export to public API for tracking agent mode transitions.
- Added mode and modeData fields to SessionContext to track active agent mode state across sessions.
- Added appendModeChange() method to SessionManager for recording mode transition entries.
- Added plan mode state restoration from session entries when resuming interactive mode sessions.
- Updated HTML export filter to treat mode_change entries as settings entries alongside model changes and thinking level changes.
- Added type validation for tool arguments in HTML export template with error handling for invalid values.
This commit is contained in:
can1357
2026-02-06 02:55:41 +01:00
parent 5339369abd
commit de397b3b04
6 changed files with 65 additions and 8 deletions
File diff suppressed because one or more lines are too long
@@ -338,7 +338,7 @@
}
// Apply filter mode
const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change'].includes(entry.type);
const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change', 'mode_change'].includes(entry.type);
let passesFilter = true;
switch (filterMode) {
+1
View File
@@ -231,6 +231,7 @@ export {
type CustomMessageEntry,
type FileEntry,
getLatestCompactionEntry,
type ModeChangeEntry,
type ModelChangeEntry,
migrateSessionEntries,
type NewSessionOptions,
@@ -345,6 +345,9 @@ export class InteractiveMode implements InteractiveModeContext {
// Initialize hooks with TUI-based UI context
await this.initHooksAndCustomTools();
// Restore mode from session (e.g. plan mode on resume)
await this.restoreModeFromSession();
// Subscribe to agent events
this.subscribeToAgent();
@@ -533,6 +536,19 @@ export class InteractiveMode implements InteractiveModeContext {
}
}
/** Restore mode state from session entries on resume (e.g. plan mode). */
private async restoreModeFromSession(): Promise<void> {
const sessionContext = this.sessionManager.buildSessionContext();
if (sessionContext.mode === "plan") {
const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
await this.enterPlanMode({ planFilePath });
} else if (sessionContext.mode === "plan_paused") {
this.planModePaused = true;
this.planModeHasEntered = true;
this.updatePlanModeStatus();
}
}
private async enterPlanMode(options?: {
planFilePath?: string;
workflow?: "parallel" | "iterative";
@@ -566,6 +582,7 @@ export class InteractiveMode implements InteractiveModeContext {
this.planModeHasEntered = true;
await this.applyPlanModeModel();
this.updatePlanModeStatus();
this.sessionManager.appendModeChange("plan", { planFilePath });
this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
}
@@ -593,8 +610,10 @@ export class InteractiveMode implements InteractiveModeContext {
this.planModePreviousTools = undefined;
this.planModePreviousModel = undefined;
this.updatePlanModeStatus();
const paused = options?.paused ?? false;
this.sessionManager.appendModeChange(paused ? "plan_paused" : "none");
if (!options?.silent) {
this.showStatus(this.planModePaused ? "Plan mode paused." : "Plan mode disabled.");
this.showStatus(paused ? "Plan mode paused." : "Plan mode disabled.");
}
}
@@ -125,6 +125,15 @@ export interface SessionInitEntry extends SessionEntryBase {
outputSchema?: unknown;
}
/** Mode change entry - tracks agent mode transitions (e.g. plan mode). */
export interface ModeChangeEntry extends SessionEntryBase {
type: "mode_change";
/** Current mode name, or "none" when exiting a mode */
mode: string;
/** Optional mode-specific data (e.g. plan file path) */
data?: Record<string, unknown>;
}
/**
* Custom message entry for extensions to inject messages into LLM context.
* Use customType to identify your extension's entries.
@@ -156,7 +165,8 @@ export type SessionEntry =
| CustomMessageEntry
| LabelEntry
| TtsrInjectionEntry
| SessionInitEntry;
| SessionInitEntry
| ModeChangeEntry;
/** Raw file entry (includes header) */
export type FileEntry = SessionHeader | SessionEntry;
@@ -176,6 +186,10 @@ export interface SessionContext {
models: Record<string, string>;
/** Names of TTSR rules that have been injected this session */
injectedTtsrRules: string[];
/** Active mode (e.g. "plan") or "none" if no special mode is active */
mode: string;
/** Mode-specific data from the last mode_change entry */
modeData?: Record<string, unknown>;
}
export interface SessionInfo {
@@ -326,7 +340,7 @@ export function buildSessionContext(
let leaf: SessionEntry | undefined;
if (leafId === null) {
// Explicitly null - return no messages (navigated to before first entry)
return { messages: [], thinkingLevel: "off", models: {}, injectedTtsrRules: [] };
return { messages: [], thinkingLevel: "off", models: {}, injectedTtsrRules: [], mode: "none" };
}
if (leafId) {
leaf = byId.get(leafId);
@@ -337,7 +351,7 @@ export function buildSessionContext(
}
if (!leaf) {
return { messages: [], thinkingLevel: "off", models: {}, injectedTtsrRules: [] };
return { messages: [], thinkingLevel: "off", models: {}, injectedTtsrRules: [], mode: "none" };
}
// Walk from leaf to root, collecting path
@@ -353,6 +367,8 @@ export function buildSessionContext(
const models: Record<string, string> = {};
let compaction: CompactionEntry | null = null;
const injectedTtsrRulesSet = new Set<string>();
let mode = "none";
let modeData: Record<string, unknown> | undefined;
for (const entry of path) {
if (entry.type === "thinking_level_change") {
@@ -373,6 +389,9 @@ export function buildSessionContext(
for (const ruleName of entry.injectedRules) {
injectedTtsrRulesSet.add(ruleName);
}
} else if (entry.type === "mode_change") {
mode = entry.mode;
modeData = entry.data;
}
}
@@ -435,7 +454,7 @@ export function buildSessionContext(
}
}
return { messages, thinkingLevel, models, injectedTtsrRules };
return { messages, thinkingLevel, models, injectedTtsrRules, mode, modeData };
}
/**
@@ -1388,6 +1407,20 @@ export class SessionManager {
return entry.id;
}
/** Append a mode change as child of current leaf, then advance leaf. Returns entry id. */
appendModeChange(mode: string, data?: Record<string, unknown>): string {
const entry: ModeChangeEntry = {
type: "mode_change",
id: generateId(this.byId),
parentId: this.leafId,
timestamp: new Date().toISOString(),
mode,
data,
};
this._appendEntry(entry);
return entry.id;
}
/**
* Append a model change as child of current leaf, then advance leaf. Returns entry id.
* @param model Model in "provider/modelId" format