Merge remote-tracking branch 'origin/main' into feat/model-cycling-enhancements

This commit is contained in:
Mario Zechner 2025-11-21 21:16:05 +01:00
commit df3af27288
18 changed files with 264 additions and 173 deletions

View file

@ -21,17 +21,6 @@ const __dirname = dirname(__filename);
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
const VERSION = packageJson.version;
const envApiKeyMap: Record<KnownProvider, string[]> = {
google: ["GEMINI_API_KEY"],
openai: ["OPENAI_API_KEY"],
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
xai: ["XAI_API_KEY"],
groq: ["GROQ_API_KEY"],
cerebras: ["CEREBRAS_API_KEY"],
openrouter: ["OPENROUTER_API_KEY"],
zai: ["ZAI_API_KEY"],
};
const defaultModelPerProvider: Record<KnownProvider, string> = {
anthropic: "claude-sonnet-4-5",
openai: "gpt-5.1-codex",
@ -478,14 +467,9 @@ async function runInteractiveMode(
scopedModels,
);
// Initialize TUI
// Initialize TUI (subscribes to agent events internally)
await renderer.init();
// Set interrupt callback
renderer.setInterruptCallback(() => {
agent.abort();
});
// Render any existing messages (from --continue mode)
renderer.renderInitialMessages(agent.state);
@ -494,12 +478,6 @@ async function runInteractiveMode(
renderer.showWarning(modelFallbackMessage);
}
// Subscribe to agent events
agent.subscribe(async (event) => {
// Pass all events to the renderer
await renderer.handleEvent(event, agent.state);
});
// Interactive loop
while (true) {
const userInput = await renderer.getUserInput();
@ -718,11 +696,6 @@ export async function main(args: string[]) {
// Load previous messages if continuing or resuming
// This may update initialModel if restoring from session
if (parsed.continue || parsed.resume) {
const messages = sessionManager.loadMessages();
if (messages.length > 0 && shouldPrintMessages) {
console.log(chalk.dim(`Loaded ${messages.length} messages from previous session`));
}
// Load and restore model (overrides initialModel if found and has API key)
const savedModel = sessionManager.loadModel();
if (savedModel) {
@ -871,9 +844,6 @@ export async function main(args: string[]) {
}
}
// Note: Session will be started lazily after first user+assistant message exchange
// (unless continuing/resuming, in which case it's already initialized)
// Log loaded context files (they're already in the system prompt)
if (shouldPrintMessages && !parsed.continue && !parsed.resume) {
const contextFiles = loadProjectContextFiles();
@ -885,19 +855,6 @@ export async function main(args: string[]) {
}
}
// Subscribe to agent events to save messages
agent.subscribe((event) => {
// Save messages on completion
if (event.type === "message_end") {
sessionManager.saveMessage(event.message);
// Check if we should initialize session now (after first user+assistant exchange)
if (sessionManager.shouldInitializeSession(agent.state.messages)) {
sessionManager.startSession(agent.state);
}
}
});
// Route to appropriate mode
if (mode === "rpc") {
// RPC mode - headless operation
@ -930,8 +887,6 @@ export async function main(args: string[]) {
}
} else {
// Parse current and last versions
const currentParts = VERSION.split(".").map(Number);
const current = { major: currentParts[0] || 0, minor: currentParts[1] || 0, patch: currentParts[2] || 0 };
const changelogPath = getChangelogPath();
const entries = parseChangelog(changelogPath);
const newEntries = getNewEntries(entries, lastVersion);

View file

@ -97,6 +97,13 @@ export class SessionManager {
this.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
}
/** Reset to a fresh session. Clears pending messages and starts a new session file. */
reset(): void {
this.pendingMessages = [];
this.sessionInitialized = false;
this.initNewSession();
}
private findMostRecentlyModifiedSession(): string | null {
try {
const files = readdirSync(this.sessionDir)

View file

@ -53,7 +53,7 @@ export class TuiRenderer {
private isInitialized = false;
private onInputCallback?: (text: string) => void;
private loadingAnimation: Loader | null = null;
private onInterruptCallback?: () => void;
private lastSigintTime = 0;
private changelogMarkdown: string | null = null;
private newVersion: string | null = null;
@ -97,6 +97,9 @@ export class TuiRenderer {
// Tool output expansion state
private toolOutputExpanded = false;
// Agent subscription unsubscribe function
private unsubscribe?: () => void;
constructor(
agent: Agent,
sessionManager: SessionManager,
@ -173,6 +176,11 @@ export class TuiRenderer {
description: "Select color theme (opens selector UI)",
};
const clearCommand: SlashCommand = {
name: "clear",
description: "Clear context and start a fresh session",
};
// Setup autocomplete for file paths and slash commands
const autocompleteProvider = new CombinedAutocompleteProvider(
[
@ -186,6 +194,7 @@ export class TuiRenderer {
loginCommand,
logoutCommand,
queueCommand,
clearCommand,
],
process.cwd(),
);
@ -268,7 +277,7 @@ export class TuiRenderer {
// Set up custom key handlers on the editor
this.editor.onEscape = () => {
// Intercept Escape key when processing
if (this.loadingAnimation && this.onInterruptCallback) {
if (this.loadingAnimation) {
// Get all queued messages
const queuedText = this.queuedMessages.join("\n\n");
@ -289,7 +298,7 @@ export class TuiRenderer {
this.agent.clearMessageQueue();
// Abort
this.onInterruptCallback();
this.agent.abort();
}
};
@ -386,6 +395,13 @@ export class TuiRenderer {
return;
}
// Check for /clear command
if (text === "/clear") {
this.handleClearCommand();
this.editor.setText("");
return;
}
// Normal message submission - validate model and API key first
const currentModel = this.agent.state.model;
if (!currentModel) {
@ -439,6 +455,9 @@ export class TuiRenderer {
this.ui.start();
this.isInitialized = true;
// Subscribe to agent events for UI updates and session saving
this.subscribeToAgent();
// Set up theme file watcher for live reload
onThemeChange(() => {
this.ui.invalidate();
@ -447,7 +466,24 @@ export class TuiRenderer {
});
}
async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
private subscribeToAgent(): void {
this.unsubscribe = this.agent.subscribe(async (event) => {
// Handle UI updates
await this.handleEvent(event, this.agent.state);
// Save messages to session
if (event.type === "message_end") {
this.sessionManager.saveMessage(event.message);
// Check if we should initialize session now (after first user+assistant exchange)
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
this.sessionManager.startSession(this.agent.state);
}
}
});
}
private async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
if (!this.isInitialized) {
await this.init();
}
@ -713,10 +749,6 @@ export class TuiRenderer {
});
}
setInterruptCallback(callback: () => void): void {
this.onInterruptCallback = callback;
}
private handleCtrlC(): void {
// Handle Ctrl+C double-press logic
const now = Date.now();
@ -1435,6 +1467,45 @@ export class TuiRenderer {
this.ui.requestRender();
}
private async handleClearCommand(): Promise<void> {
// Unsubscribe first to prevent processing abort events
this.unsubscribe?.();
// Abort and wait for completion
this.agent.abort();
await this.agent.waitForIdle();
// Stop loading animation
if (this.loadingAnimation) {
this.loadingAnimation.stop();
this.loadingAnimation = null;
}
this.statusContainer.clear();
// Reset agent and session
this.agent.reset();
this.sessionManager.reset();
// Resubscribe to agent
this.subscribeToAgent();
// Clear UI state
this.chatContainer.clear();
this.pendingMessagesContainer.clear();
this.queuedMessages = [];
this.streamingComponent = null;
this.pendingTools.clear();
this.isFirstUserMessage = true;
// Show confirmation
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(
new Text(theme.fg("accent", "✓ Context cleared") + "\n" + theme.fg("muted", "Started fresh session"), 1, 1),
);
this.ui.requestRender();
}
private updatePendingMessagesDisplay(): void {
this.pendingMessagesContainer.clear();