refactor(coding-agent): add runtime host for session switching closes #2024
This commit is contained in:
@@ -38,6 +38,10 @@ export class FooterComponent implements Component {
|
||||
private footerData: ReadonlyFooterDataProvider,
|
||||
) {}
|
||||
|
||||
setSession(session: AgentSession): void {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
setAutoCompactEnabled(enabled: boolean): void {
|
||||
this.autoCompactEnabled = enabled;
|
||||
}
|
||||
@@ -86,7 +90,7 @@ export class FooterComponent implements Component {
|
||||
const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
||||
|
||||
// Replace home directory with ~
|
||||
let pwd = process.cwd();
|
||||
let pwd = this.session.sessionManager.getCwd();
|
||||
const home = process.env.HOME || process.env.USERPROFILE;
|
||||
if (home && pwd.startsWith(home)) {
|
||||
pwd = `~${pwd.slice(home.length)}`;
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
VERSION,
|
||||
} from "../../config.js";
|
||||
import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js";
|
||||
import type { AgentSessionRuntimeHost } from "../../core/agent-session-runtime.js";
|
||||
import type {
|
||||
ExtensionContext,
|
||||
ExtensionRunner,
|
||||
@@ -144,7 +145,7 @@ export interface InteractiveModeOptions {
|
||||
}
|
||||
|
||||
export class InteractiveMode {
|
||||
private session: AgentSession;
|
||||
private runtimeHost: AgentSessionRuntimeHost;
|
||||
private ui: TUI;
|
||||
private chatContainer: Container;
|
||||
private pendingMessagesContainer: Container;
|
||||
@@ -242,6 +243,9 @@ export class InteractiveMode {
|
||||
private customHeader: (Component & { dispose?(): void }) | undefined = undefined;
|
||||
|
||||
// Convenience accessors
|
||||
private get session(): AgentSession {
|
||||
return this.runtimeHost.session;
|
||||
}
|
||||
private get agent() {
|
||||
return this.session.agent;
|
||||
}
|
||||
@@ -253,10 +257,10 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
constructor(
|
||||
session: AgentSession,
|
||||
runtimeHost: AgentSessionRuntimeHost,
|
||||
private options: InteractiveModeOptions = {},
|
||||
) {
|
||||
this.session = session;
|
||||
this.runtimeHost = runtimeHost;
|
||||
this.version = VERSION;
|
||||
this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
|
||||
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
||||
@@ -277,9 +281,9 @@ export class InteractiveMode {
|
||||
this.editor = this.defaultEditor;
|
||||
this.editorContainer = new Container();
|
||||
this.editorContainer.addChild(this.editor as Component);
|
||||
this.footerDataProvider = new FooterDataProvider();
|
||||
this.footer = new FooterComponent(session, this.footerDataProvider);
|
||||
this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
|
||||
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
||||
this.footer = new FooterComponent(this.session, this.footerDataProvider);
|
||||
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
||||
|
||||
// Load hide thinking block setting
|
||||
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
||||
@@ -412,7 +416,7 @@ export class InteractiveMode {
|
||||
// Setup autocomplete
|
||||
this.autocompleteProvider = new CombinedAutocompleteProvider(
|
||||
[...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList],
|
||||
process.cwd(),
|
||||
this.sessionManager.getCwd(),
|
||||
fdPath,
|
||||
);
|
||||
this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
|
||||
@@ -524,7 +528,7 @@ export class InteractiveMode {
|
||||
this.isInitialized = true;
|
||||
|
||||
// Initialize extensions first so resources are shown before messages
|
||||
await this.initExtensions();
|
||||
await this.bindCurrentSessionExtensions();
|
||||
|
||||
// Render initial messages AFTER showing loaded resources
|
||||
this.renderInitialMessages();
|
||||
@@ -555,7 +559,7 @@ export class InteractiveMode {
|
||||
* Update terminal title with session name and cwd.
|
||||
*/
|
||||
private updateTerminalTitle(): void {
|
||||
const cwdBasename = path.basename(process.cwd());
|
||||
const cwdBasename = path.basename(this.sessionManager.getCwd());
|
||||
const sessionName = this.sessionManager.getSessionName();
|
||||
if (sessionName) {
|
||||
this.ui.terminal.setTitle(`π - ${sessionName} - ${cwdBasename}`);
|
||||
@@ -673,7 +677,7 @@ export class InteractiveMode {
|
||||
|
||||
try {
|
||||
const packageManager = new DefaultPackageManager({
|
||||
cwd: process.cwd(),
|
||||
cwd: this.sessionManager.getCwd(),
|
||||
agentDir: getAgentDir(),
|
||||
settingsManager: this.settingsManager,
|
||||
});
|
||||
@@ -1161,7 +1165,7 @@ export class InteractiveMode {
|
||||
/**
|
||||
* Initialize the extension system with TUI-based UI context.
|
||||
*/
|
||||
private async initExtensions(): Promise<void> {
|
||||
private async bindCurrentSessionExtensions(): Promise<void> {
|
||||
const uiContext = this.createExtensionUIContext();
|
||||
await this.session.bindExtensions({
|
||||
uiContext,
|
||||
@@ -1173,39 +1177,23 @@ export class InteractiveMode {
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Delegate to AgentSession (handles setup + agent state sync)
|
||||
const success = await this.session.newSession(options);
|
||||
if (!success) {
|
||||
return { cancelled: true };
|
||||
const result = await this.runtimeHost.newSession(options);
|
||||
if (!result.cancelled) {
|
||||
await this.handleRuntimeSessionChange();
|
||||
this.renderCurrentSessionState();
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
// Clear UI state
|
||||
this.chatContainer.clear();
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
||||
// Render any messages added via setup, or show empty session
|
||||
this.renderInitialMessages();
|
||||
this.ui.requestRender();
|
||||
|
||||
return { cancelled: false };
|
||||
return result;
|
||||
},
|
||||
fork: async (entryId) => {
|
||||
const result = await this.session.fork(entryId);
|
||||
if (result.cancelled) {
|
||||
return { cancelled: true };
|
||||
const result = await this.runtimeHost.fork(entryId);
|
||||
if (!result.cancelled) {
|
||||
await this.handleRuntimeSessionChange();
|
||||
this.renderCurrentSessionState();
|
||||
this.editor.setText(result.selectedText ?? "");
|
||||
this.showStatus("Forked to new session");
|
||||
}
|
||||
|
||||
this.chatContainer.clear();
|
||||
this.renderInitialMessages();
|
||||
this.editor.setText(result.selectedText);
|
||||
this.showStatus("Forked to new session");
|
||||
|
||||
return { cancelled: false };
|
||||
return { cancelled: result.cancelled };
|
||||
},
|
||||
navigateTree: async (targetId, options) => {
|
||||
const result = await this.session.navigateTree(targetId, {
|
||||
@@ -1224,7 +1212,6 @@ export class InteractiveMode {
|
||||
this.editor.setText(result.editorText);
|
||||
}
|
||||
this.showStatus("Navigated to selected point");
|
||||
|
||||
return { cancelled: false };
|
||||
},
|
||||
switchSession: async (sessionPath) => {
|
||||
@@ -1259,6 +1246,45 @@ export class InteractiveMode {
|
||||
this.showLoadedResources({ force: false });
|
||||
}
|
||||
|
||||
private applyRuntimeSettings(): void {
|
||||
this.footer.setSession(this.session);
|
||||
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
||||
this.footerDataProvider.setCwd(this.sessionManager.getCwd());
|
||||
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
||||
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
||||
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
||||
const editorPaddingX = this.settingsManager.getEditorPaddingX();
|
||||
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
|
||||
this.defaultEditor.setPaddingX(editorPaddingX);
|
||||
this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
|
||||
if (this.editor !== this.defaultEditor) {
|
||||
this.editor.setPaddingX?.(editorPaddingX);
|
||||
this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRuntimeSessionChange(): Promise<void> {
|
||||
this.resetExtensionUI();
|
||||
this.unsubscribe?.();
|
||||
this.unsubscribe = undefined;
|
||||
this.applyRuntimeSettings();
|
||||
await this.bindCurrentSessionExtensions();
|
||||
this.subscribeToAgent();
|
||||
await this.updateAvailableProviderCount();
|
||||
this.updateEditorBorderColor();
|
||||
this.updateTerminalTitle();
|
||||
}
|
||||
|
||||
private renderCurrentSessionState(): void {
|
||||
this.chatContainer.clear();
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
this.renderInitialMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registered tool definition by name (for custom rendering).
|
||||
*/
|
||||
@@ -1277,7 +1303,7 @@ export class InteractiveMode {
|
||||
const createContext = (): ExtensionContext => ({
|
||||
ui: this.createExtensionUIContext(),
|
||||
hasUI: true,
|
||||
cwd: process.cwd(),
|
||||
cwd: this.sessionManager.getCwd(),
|
||||
sessionManager: this.sessionManager,
|
||||
modelRegistry: this.session.modelRegistry,
|
||||
model: this.session.model,
|
||||
@@ -2305,6 +2331,7 @@ export class InteractiveMode {
|
||||
},
|
||||
this.getRegisteredToolDefinition(content.name),
|
||||
this.ui,
|
||||
this.sessionManager.getCwd(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
@@ -2372,6 +2399,7 @@ export class InteractiveMode {
|
||||
},
|
||||
this.getRegisteredToolDefinition(event.toolName),
|
||||
this.ui,
|
||||
this.sessionManager.getCwd(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
@@ -2681,6 +2709,7 @@ export class InteractiveMode {
|
||||
{ showImages: this.settingsManager.getShowImages() },
|
||||
this.getRegisteredToolDefinition(content.name),
|
||||
this.ui,
|
||||
this.sessionManager.getCwd(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
@@ -2779,14 +2808,7 @@ export class InteractiveMode {
|
||||
private async shutdown(): Promise<void> {
|
||||
if (this.isShuttingDown) return;
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// Emit shutdown event to extensions
|
||||
const extensionRunner = this.session.extensionRunner;
|
||||
if (extensionRunner?.hasHandlers("session_shutdown")) {
|
||||
await extensionRunner.emit({
|
||||
type: "session_shutdown",
|
||||
});
|
||||
}
|
||||
await this.runtimeHost.dispose();
|
||||
|
||||
// Wait for any pending renders to complete
|
||||
// requestRender() uses process.nextTick(), so we wait one tick
|
||||
@@ -3605,17 +3627,15 @@ export class InteractiveMode {
|
||||
const selector = new UserMessageSelectorComponent(
|
||||
userMessages.map((m) => ({ id: m.entryId, text: m.text })),
|
||||
async (entryId) => {
|
||||
const result = await this.session.fork(entryId);
|
||||
const result = await this.runtimeHost.fork(entryId);
|
||||
if (result.cancelled) {
|
||||
// Extension cancelled the fork
|
||||
done();
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatContainer.clear();
|
||||
this.renderInitialMessages();
|
||||
this.editor.setText(result.selectedText);
|
||||
await this.handleRuntimeSessionChange();
|
||||
this.renderCurrentSessionState();
|
||||
this.editor.setText(result.selectedText ?? "");
|
||||
done();
|
||||
this.showStatus("Branched to new session");
|
||||
},
|
||||
@@ -3792,26 +3812,17 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
private async handleResumeSession(sessionPath: string): Promise<void> {
|
||||
// Stop loading animation
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Clear UI state
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
||||
// Switch session via AgentSession (emits extension session events)
|
||||
await this.session.switchSession(sessionPath);
|
||||
|
||||
// Clear and re-render the chat
|
||||
this.chatContainer.clear();
|
||||
this.renderInitialMessages();
|
||||
const result = await this.runtimeHost.switchSession(sessionPath);
|
||||
if (result.cancelled) {
|
||||
return;
|
||||
}
|
||||
await this.handleRuntimeSessionChange();
|
||||
this.renderCurrentSessionState();
|
||||
this.showStatus("Resumed session");
|
||||
}
|
||||
|
||||
@@ -4063,29 +4074,18 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
try {
|
||||
// Stop loading animation
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Clear UI state
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
||||
const success = await this.session.importFromJsonl(inputPath);
|
||||
if (!success) {
|
||||
this.showWarning("Import cancelled");
|
||||
const result = await this.runtimeHost.importFromJsonl(inputPath);
|
||||
if (result.cancelled) {
|
||||
this.showStatus("Import cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear and re-render the chat
|
||||
this.chatContainer.clear();
|
||||
this.renderInitialMessages();
|
||||
await this.handleRuntimeSessionChange();
|
||||
this.renderCurrentSessionState();
|
||||
this.showStatus(`Session imported from: ${inputPath}`);
|
||||
} catch (error: unknown) {
|
||||
this.showError(`Failed to import session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
@@ -4427,25 +4427,17 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
private async handleClearCommand(): Promise<void> {
|
||||
// Stop loading animation
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// New session via session (emits extension session events)
|
||||
await this.session.newSession();
|
||||
|
||||
// Clear UI state
|
||||
this.headerContainer.clear();
|
||||
this.chatContainer.clear();
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
||||
const result = await this.runtimeHost.newSession();
|
||||
if (result.cancelled) {
|
||||
return;
|
||||
}
|
||||
await this.handleRuntimeSessionChange();
|
||||
this.renderCurrentSessionState();
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
||||
this.ui.requestRender();
|
||||
@@ -4511,7 +4503,7 @@ export class InteractiveMode {
|
||||
type: "user_bash",
|
||||
command,
|
||||
excludeFromContext,
|
||||
cwd: process.cwd(),
|
||||
cwd: this.sessionManager.getCwd(),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user