diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 6392308c..6f32cbb3 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3620,7 +3620,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 196608, - maxTokens: 131072, + maxTokens: 65536, } satisfies Model<"openai-completions">, "deepcogito/cogito-v2-preview-llama-405b": { id: "deepcogito/cogito-v2-preview-llama-405b", @@ -4623,7 +4623,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 128000, + maxTokens: 131072, } satisfies Model<"openai-completions">, "openai/gpt-oss-20b": { id: "openai/gpt-oss-20b", diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index e9f24225..f42ee625 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1583,7 +1583,7 @@ export class AgentSession { targetId: string, options: { summarize?: boolean; customInstructions?: string } = {}, ): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean }> { - const oldLeafId = this.sessionManager.getLeafUuid(); + const oldLeafId = this.sessionManager.getLeafId(); // No-op if already at target if (targetId === oldLeafId) { @@ -1661,21 +1661,19 @@ export class AgentSession { // Run default summarizer if needed let summaryText: string | undefined; if (options.summarize && entriesToSummarize.length > 0 && !hookSummary) { - try { - summaryText = await this._generateBranchSummary( - entriesToSummarize, - options.customInstructions, - this._branchSummaryAbortController.signal, - ); - } catch (error) { - this._branchSummaryAbortController = undefined; - // Check if aborted - if (error instanceof Error && (error.name === "AbortError" || error.message === "aborted")) { - return { cancelled: true, aborted: true }; - } - // Re-throw actual errors so UI can display them - throw error; + const result = await this._generateBranchSummary( + entriesToSummarize, + options.customInstructions, + this._branchSummaryAbortController.signal, + ); + this._branchSummaryAbortController = undefined; + if (result.aborted) { + return { cancelled: true, aborted: true }; } + if (result.error) { + throw new Error(result.error); + } + summaryText = result.summary; } else if (hookSummary) { summaryText = hookSummary.summary; } @@ -1704,14 +1702,17 @@ export class AgentSession { } // Switch leaf (with or without summary) + // Summary is attached at the navigation target position (newLeafId), not the old branch let summaryEntry: BranchSummaryEntry | undefined; - if (newLeafId === null) { - // Navigating to root user message - reset leaf to empty - this.sessionManager.resetLeaf(); - } else if (summaryText) { + if (summaryText) { + // Create summary at target position (can be null for root) const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText); summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry; + } else if (newLeafId === null) { + // No summary, navigating to root - reset leaf + this.sessionManager.resetLeaf(); } else { + // No summary, navigating to non-root this.sessionManager.branch(newLeafId); } @@ -1723,7 +1724,7 @@ export class AgentSession { if (this._hookRunner) { await this._hookRunner.emit({ type: "session_tree", - newLeafId: this.sessionManager.getLeafUuid(), + newLeafId: this.sessionManager.getLeafId(), oldLeafId, summaryEntry, fromHook: summaryText ? fromHook : undefined, @@ -1744,7 +1745,7 @@ export class AgentSession { entries: SessionEntry[], customInstructions: string | undefined, signal: AbortSignal, - ): Promise { + ): Promise<{ summary?: string; aborted?: boolean; error?: string }> { // Convert entries to messages for summarization const messages: Array<{ role: string; content: string }> = []; for (const entry of entries) { @@ -1770,7 +1771,7 @@ export class AgentSession { } if (messages.length === 0) { - return "No content to summarize"; + return { summary: "No content to summarize" }; } // Build prompt for summarization @@ -1804,12 +1805,20 @@ export class AgentSession { { apiKey, signal, maxTokens: 1024 }, ); + // Check if aborted or errored + if (response.stopReason === "aborted") { + return { aborted: true }; + } + if (response.stopReason === "error") { + return { error: response.errorMessage || "Summarization failed" }; + } + const summary = response.content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join("\n"); - return summary || "No summary generated"; + return { summary: summary || "No summary generated" }; } /** diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 847a9134..e3d20e31 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -25,7 +25,7 @@ export type ReadonlySessionManager = Pick< | "getSessionDir" | "getSessionId" | "getSessionFile" - | "getLeafUuid" + | "getLeafId" | "getLeafEntry" | "getEntry" | "getLabel" diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 5d6b6c90..ef33f15d 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -684,7 +684,7 @@ export class SessionManager { // Tree Traversal // ========================================================================= - getLeafUuid(): string | null { + getLeafId(): string | null { return this.leafId; } @@ -845,8 +845,8 @@ export class SessionManager { * Same as branch(), but also appends a branch_summary entry that captures * context from the abandoned conversation path. */ - branchWithSummary(branchFromId: string, summary: string): string { - if (!this.byId.has(branchFromId)) { + branchWithSummary(branchFromId: string | null, summary: string): string { + if (branchFromId !== null && !this.byId.has(branchFromId)) { throw new Error(`Entry ${branchFromId} not found`); } this.leafId = branchFromId; @@ -855,7 +855,7 @@ export class SessionManager { id: generateId(this.byId), parentId: branchFromId, timestamp: new Date().toISOString(), - fromId: branchFromId, + fromId: branchFromId ?? "root", summary, }; this._appendEntry(entry); diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts index 18c3ca8b..15945373 100644 --- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts @@ -609,8 +609,12 @@ class TreeList implements Component { return `[edit: ${path}]`; } case "bash": { - const cmd = String(args.command || "").slice(0, 50); - return `[bash: ${cmd}${(args.command as string)?.length > 50 ? "..." : ""}]`; + const rawCmd = String(args.command || ""); + const cmd = rawCmd + .replace(/[\n\t]/g, " ") + .trim() + .slice(0, 50); + return `[bash: ${cmd}${rawCmd.length > 50 ? "..." : ""}]`; } case "grep": { const pattern = String(args.pattern || ""); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index bad29cc2..bd680fdf 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -1600,7 +1600,7 @@ export class InteractiveMode { private showTreeSelector(): void { const tree = this.sessionManager.getTree(); - const realLeafId = this.sessionManager.getLeafUuid(); + const realLeafId = this.sessionManager.getLeafId(); // Find the visible leaf for display (skip metadata entries like labels) let visibleLeafId = realLeafId; @@ -1660,7 +1660,9 @@ export class InteractiveMode { const result = await this.session.navigateTree(entryId, { summarize: wantsSummary }); if (result.aborted) { + // Summarization aborted - re-show tree selector this.showStatus("Branch summarization cancelled"); + this.showTreeSelector(); return; } if (result.cancelled) { diff --git a/packages/coding-agent/test/session-manager/tree-traversal.test.ts b/packages/coding-agent/test/session-manager/tree-traversal.test.ts index 8cf1dcde..93715ed6 100644 --- a/packages/coding-agent/test/session-manager/tree-traversal.test.ts +++ b/packages/coding-agent/test/session-manager/tree-traversal.test.ts @@ -129,16 +129,16 @@ describe("SessionManager append and tree traversal", () => { it("leaf pointer advances after each append", () => { const session = SessionManager.inMemory(); - expect(session.getLeafUuid()).toBe(""); + expect(session.getLeafId()).toBe(""); const id1 = session.appendMessage(userMsg("1")); - expect(session.getLeafUuid()).toBe(id1); + expect(session.getLeafId()).toBe(id1); const id2 = session.appendMessage(assistantMsg("2")); - expect(session.getLeafUuid()).toBe(id2); + expect(session.getLeafId()).toBe(id2); const id3 = session.appendThinkingLevelChange("high"); - expect(session.getLeafUuid()).toBe(id3); + expect(session.getLeafId()).toBe(id3); }); }); @@ -303,10 +303,10 @@ describe("SessionManager append and tree traversal", () => { const _id2 = session.appendMessage(assistantMsg("2")); const id3 = session.appendMessage(userMsg("3")); - expect(session.getLeafUuid()).toBe(id3); + expect(session.getLeafId()).toBe(id3); session.branch(id1); - expect(session.getLeafUuid()).toBe(id1); + expect(session.getLeafId()).toBe(id1); }); it("throws for non-existent entry", () => { @@ -341,7 +341,7 @@ describe("SessionManager append and tree traversal", () => { const summaryId = session.branchWithSummary(id1, "Summary of abandoned work"); - expect(session.getLeafUuid()).toBe(summaryId); + expect(session.getLeafId()).toBe(summaryId); const entries = session.getEntries(); const summaryEntry = entries.find((e) => e.type === "branch_summary");