Fix tree selector gutter alignment, add page navigation, improve styling

- Fix gutter/connector alignment by tracking gutter positions with GutterInfo
- Convert recursive markContains to iterative to avoid stack overflow
- Add left/right arrow keys for page up/down navigation
- Consistent assistant message styling (green prefix, appropriate status colors)
- Remove leaf marker (*), active path bullets are sufficient
- Add Expandable interface for toggle expansion
- Fix BranchSummaryMessageComponent expansion toggle
This commit is contained in:
Mario Zechner 2025-12-29 18:57:15 +01:00
parent 975e90ea8c
commit 159e19a010
5 changed files with 186 additions and 104 deletions

View file

@ -3620,7 +3620,7 @@ export const MODELS = {
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 196608, contextWindow: 196608,
maxTokens: 131072, maxTokens: 65536,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"deepcogito/cogito-v2-preview-llama-405b": { "deepcogito/cogito-v2-preview-llama-405b": {
id: "deepcogito/cogito-v2-preview-llama-405b", id: "deepcogito/cogito-v2-preview-llama-405b",
@ -4623,7 +4623,7 @@ export const MODELS = {
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 131072, contextWindow: 131072,
maxTokens: 128000, maxTokens: 131072,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-oss-20b": { "openai/gpt-oss-20b": {
id: "openai/gpt-oss-20b", id: "openai/gpt-oss-20b",
@ -6104,9 +6104,9 @@ export const MODELS = {
contextWindow: 32768, contextWindow: 32768,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"anthropic/claude-3.5-haiku": { "anthropic/claude-3.5-haiku-20241022": {
id: "anthropic/claude-3.5-haiku", id: "anthropic/claude-3.5-haiku-20241022",
name: "Anthropic: Claude 3.5 Haiku", name: "Anthropic: Claude 3.5 Haiku (2024-10-22)",
api: "openai-completions", api: "openai-completions",
provider: "openrouter", provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1", baseUrl: "https://openrouter.ai/api/v1",
@ -6121,9 +6121,9 @@ export const MODELS = {
contextWindow: 200000, contextWindow: 200000,
maxTokens: 8192, maxTokens: 8192,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"anthropic/claude-3.5-haiku-20241022": { "anthropic/claude-3.5-haiku": {
id: "anthropic/claude-3.5-haiku-20241022", id: "anthropic/claude-3.5-haiku",
name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", name: "Anthropic: Claude 3.5 Haiku",
api: "openai-completions", api: "openai-completions",
provider: "openrouter", provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1", baseUrl: "https://openrouter.ai/api/v1",
@ -6359,6 +6359,23 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 16384, maxTokens: 16384,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": { "meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct", id: "meta-llama/llama-3.1-405b-instruct",
name: "Meta: Llama 3.1 405B Instruct", name: "Meta: Llama 3.1 405B Instruct",
@ -6393,23 +6410,6 @@ export const MODELS = {
contextWindow: 131072, contextWindow: 131072,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mistral-nemo": { "mistralai/mistral-nemo": {
id: "mistralai/mistral-nemo", id: "mistralai/mistral-nemo",
name: "Mistral: Mistral Nemo", name: "Mistral: Mistral Nemo",
@ -6546,6 +6546,23 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o": { "openai/gpt-4o": {
id: "openai/gpt-4o", id: "openai/gpt-4o",
name: "OpenAI: GPT-4o", name: "OpenAI: GPT-4o",
@ -6580,23 +6597,6 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-70b-instruct": { "meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct", id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct", name: "Meta: Llama 3 70B Instruct",
@ -6835,23 +6835,6 @@ export const MODELS = {
contextWindow: 8191, contextWindow: 8191,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo": {
id: "openai/gpt-3.5-turbo",
name: "OpenAI: GPT-3.5 Turbo",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4": { "openai/gpt-4": {
id: "openai/gpt-4", id: "openai/gpt-4",
name: "OpenAI: GPT-4", name: "OpenAI: GPT-4",
@ -6869,6 +6852,23 @@ export const MODELS = {
contextWindow: 8191, contextWindow: 8191,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo": {
id: "openai/gpt-3.5-turbo",
name: "OpenAI: GPT-3.5 Turbo",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openrouter/auto": { "openrouter/auto": {
id: "openrouter/auto", id: "openrouter/auto",
name: "OpenRouter: Auto Router", name: "OpenRouter: Auto Router",

View file

@ -23,8 +23,13 @@ function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string {
/** /**
* Generate a unified diff string with line numbers and context * Generate a unified diff string with line numbers and context
* Returns both the diff string and the first changed line number (in the new file)
*/ */
function generateDiffString(oldContent: string, newContent: string, contextLines = 4): string { function generateDiffString(
oldContent: string,
newContent: string,
contextLines = 4,
): { diff: string; firstChangedLine: number | undefined } {
const parts = Diff.diffLines(oldContent, newContent); const parts = Diff.diffLines(oldContent, newContent);
const output: string[] = []; const output: string[] = [];
@ -36,6 +41,7 @@ function generateDiffString(oldContent: string, newContent: string, contextLines
let oldLineNum = 1; let oldLineNum = 1;
let newLineNum = 1; let newLineNum = 1;
let lastWasChange = false; let lastWasChange = false;
let firstChangedLine: number | undefined;
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
const part = parts[i]; const part = parts[i];
@ -45,6 +51,11 @@ function generateDiffString(oldContent: string, newContent: string, contextLines
} }
if (part.added || part.removed) { if (part.added || part.removed) {
// Capture the first changed line (in the new file)
if (firstChangedLine === undefined) {
firstChangedLine = newLineNum;
}
// Show the change // Show the change
for (const line of raw) { for (const line of raw) {
if (part.added) { if (part.added) {
@ -113,7 +124,7 @@ function generateDiffString(oldContent: string, newContent: string, contextLines
} }
} }
return output.join("\n"); return { diff: output.join("\n"), firstChangedLine };
} }
const editSchema = Type.Object({ const editSchema = Type.Object({
@ -125,6 +136,8 @@ const editSchema = Type.Object({
export interface EditToolDetails { export interface EditToolDetails {
/** Unified diff of the changes made */ /** Unified diff of the changes made */
diff: string; diff: string;
/** Line number of the first change in the new file (for editor navigation) */
firstChangedLine?: number;
} }
export function createEditTool(cwd: string): AgentTool<typeof editSchema> { export function createEditTool(cwd: string): AgentTool<typeof editSchema> {
@ -143,7 +156,7 @@ export function createEditTool(cwd: string): AgentTool<typeof editSchema> {
return new Promise<{ return new Promise<{
content: Array<{ type: "text"; text: string }>; content: Array<{ type: "text"; text: string }>;
details: { diff: string } | undefined; details: EditToolDetails | undefined;
}>((resolve, reject) => { }>((resolve, reject) => {
// Check if already aborted // Check if already aborted
if (signal?.aborted) { if (signal?.aborted) {
@ -262,6 +275,7 @@ export function createEditTool(cwd: string): AgentTool<typeof editSchema> {
signal.removeEventListener("abort", onAbort); signal.removeEventListener("abort", onAbort);
} }
const diffResult = generateDiffString(normalizedContent, normalizedNewContent);
resolve({ resolve({
content: [ content: [
{ {
@ -269,7 +283,7 @@ export function createEditTool(cwd: string): AgentTool<typeof editSchema> {
text: `Successfully replaced text in ${path}.`, text: `Successfully replaced text in ${path}.`,
}, },
], ],
details: { diff: generateDiffString(normalizedContent, normalizedNewContent) }, details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine },
}); });
} catch (error: any) { } catch (error: any) {
// Clean up abort handler // Clean up abort handler

View file

@ -415,10 +415,14 @@ export class ToolExecutionComponent extends Container {
} else if (this.toolName === "edit") { } else if (this.toolName === "edit") {
const rawPath = this.args?.file_path || this.args?.path || ""; const rawPath = this.args?.file_path || this.args?.path || "";
const path = shortenPath(rawPath); const path = shortenPath(rawPath);
text =
theme.fg("toolTitle", theme.bold("edit")) + // Build path display, appending :line if we have a successful result with line info
" " + let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
(path ? theme.fg("accent", path) : theme.fg("toolOutput", "...")); if (this.result && !this.result.isError && this.result.details?.firstChangedLine) {
pathDisplay += theme.fg("warning", `:${this.result.details.firstChangedLine}`);
}
text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
if (this.result) { if (this.result) {
if (this.result.isError) { if (this.result.isError) {

View file

@ -3,6 +3,8 @@ import {
Container, Container,
Input, Input,
isArrowDown, isArrowDown,
isArrowLeft,
isArrowRight,
isArrowUp, isArrowUp,
isBackspace, isBackspace,
isCtrlC, isCtrlC,
@ -18,17 +20,23 @@ import type { SessionTreeNode } from "../../../core/session-manager.js";
import { theme } from "../theme/theme.js"; import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js"; import { DynamicBorder } from "./dynamic-border.js";
/** Gutter info: position (displayIndent where connector was) and whether to show │ */
interface GutterInfo {
position: number; // displayIndent level where the connector was shown
show: boolean; // true = show │, false = show spaces
}
/** Flattened tree node for navigation */ /** Flattened tree node for navigation */
interface FlatNode { interface FlatNode {
node: SessionTreeNode; node: SessionTreeNode;
/** Indentation level (each level = 2 spaces) */ /** Indentation level (each level = 3 chars) */
indent: number; indent: number;
/** Whether to show connector (├─ or └─) - true if parent has multiple children */ /** Whether to show connector (├─ or └─) - true if parent has multiple children */
showConnector: boolean; showConnector: boolean;
/** If showConnector, true = last sibling (└─), false = not last (├─) */ /** If showConnector, true = last sibling (└─), false = not last (├─) */
isLast: boolean; isLast: boolean;
/** For each ancestor branch point, true = show │ (more siblings below), false = show space */ /** Gutter info for each ancestor branch point */
gutters: boolean[]; gutters: GutterInfo[];
/** True if this node is a root under a virtual branching root (multiple roots) */ /** True if this node is a root under a virtual branching root (multiple roots) */
isVirtualRootChild: boolean; isVirtualRootChild: boolean;
} }
@ -109,24 +117,36 @@ class TreeList implements Component {
// - At indent 2+: stay flat for single-child chains, +1 only if parent branches // - At indent 2+: stay flat for single-child chains, +1 only if parent branches
// Stack items: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] // Stack items: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild]
type StackItem = [SessionTreeNode, number, boolean, boolean, boolean, boolean[], boolean]; type StackItem = [SessionTreeNode, number, boolean, boolean, boolean, GutterInfo[], boolean];
const stack: StackItem[] = []; const stack: StackItem[] = [];
// Determine which subtrees contain the active leaf (to sort current branch first) // Determine which subtrees contain the active leaf (to sort current branch first)
// Use iterative post-order traversal to avoid stack overflow
const containsActive = new Map<SessionTreeNode, boolean>(); const containsActive = new Map<SessionTreeNode, boolean>();
const leafId = this.currentLeafId; const leafId = this.currentLeafId;
const markContains = (node: SessionTreeNode): boolean => { {
let has = leafId !== null && node.entry.id === leafId; // Build list in pre-order, then process in reverse for post-order effect
for (const child of node.children) { const allNodes: SessionTreeNode[] = [];
if (markContains(child)) { const preOrderStack: SessionTreeNode[] = [...roots];
has = true; while (preOrderStack.length > 0) {
const node = preOrderStack.pop()!;
allNodes.push(node);
// Push children in reverse so they're processed left-to-right
for (let i = node.children.length - 1; i >= 0; i--) {
preOrderStack.push(node.children[i]);
} }
} }
containsActive.set(node, has); // Process in reverse (post-order): children before parents
return has; for (let i = allNodes.length - 1; i >= 0; i--) {
}; const node = allNodes[i];
for (const root of roots) { let has = leafId !== null && node.entry.id === leafId;
markContains(root); for (const child of node.children) {
if (containsActive.get(child)) {
has = true;
}
}
containsActive.set(node, has);
}
} }
// Add roots in reverse order, prioritizing the one containing the active leaf // Add roots in reverse order, prioritizing the one containing the active leaf
@ -189,7 +209,15 @@ class TreeList implements Component {
// Build gutters for children // Build gutters for children
// If this node showed a connector, add a gutter entry for descendants // If this node showed a connector, add a gutter entry for descendants
const childGutters = showConnector ? [...gutters, !isLast] : gutters; // Only add gutter if connector is actually displayed (not suppressed for virtual root children)
const connectorDisplayed = showConnector && !isVirtualRootChild;
// When connector is displayed, add a gutter entry at the connector's position
// Connector is at position (displayIndent - 1), so gutter should be there too
const currentDisplayIndent = this.multipleRoots ? Math.max(0, indent - 1) : indent;
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
const childGutters: GutterInfo[] = connectorDisplayed
? [...gutters, { position: connectorPosition, show: !isLast }]
: gutters;
// Add children in reverse order // Add children in reverse order
for (let i = orderedChildren.length - 1; i >= 0; i--) { for (let i = orderedChildren.length - 1; i >= 0; i--) {
@ -378,22 +406,48 @@ class TreeList implements Component {
const flatNode = this.filteredNodes[i]; const flatNode = this.filteredNodes[i];
const entry = flatNode.node.entry; const entry = flatNode.node.entry;
const isSelected = i === this.selectedIndex; const isSelected = i === this.selectedIndex;
const isCurrentLeaf = entry.id === this.currentLeafId;
// Build line: cursor + gutters + connector + extra indent + label + content + suffix // Build line: cursor + prefix + path marker + label + content
const cursor = isSelected ? theme.fg("accent", " ") : " "; const cursor = isSelected ? theme.fg("accent", " ") : " ";
// If multiple roots, shift display (roots at 0, not 1) // If multiple roots, shift display (roots at 0, not 1)
const displayIndent = this.multipleRoots ? Math.max(0, flatNode.indent - 1) : flatNode.indent; const displayIndent = this.multipleRoots ? Math.max(0, flatNode.indent - 1) : flatNode.indent;
// Build prefix: gutters + connector + extra spaces // Build prefix with gutters at their correct positions
const gutterStr = flatNode.gutters.map((g) => (g ? "│ " : " ")).join(""); // Each gutter has a position (displayIndent where its connector was shown)
const connector = const connector =
flatNode.showConnector && !flatNode.isVirtualRootChild ? (flatNode.isLast ? "└─ " : "├─ ") : ""; flatNode.showConnector && !flatNode.isVirtualRootChild ? (flatNode.isLast ? "└─ " : "├─ ") : "";
// Extra indent for visual grouping beyond gutters/connector const connectorPosition = connector ? displayIndent - 1 : -1;
const prefixLevels = flatNode.gutters.length + (connector ? 1 : 0);
const extraIndent = " ".repeat(Math.max(0, displayIndent - prefixLevels)); // Build prefix char by char, placing gutters and connector at their positions
const prefix = gutterStr + connector + extraIndent; const totalChars = displayIndent * 3;
const prefixChars: string[] = [];
for (let i = 0; i < totalChars; i++) {
const level = Math.floor(i / 3);
const posInLevel = i % 3;
// Check if there's a gutter at this level
const gutter = flatNode.gutters.find((g) => g.position === level);
if (gutter) {
if (posInLevel === 0) {
prefixChars.push(gutter.show ? "│" : " ");
} else {
prefixChars.push(" ");
}
} else if (connector && level === connectorPosition) {
// Connector at this level
if (posInLevel === 0) {
prefixChars.push(flatNode.isLast ? "└" : "├");
} else if (posInLevel === 1) {
prefixChars.push("─");
} else {
prefixChars.push(" ");
}
} else {
prefixChars.push(" ");
}
}
const prefix = prefixChars.join("");
// Active path marker - shown right before the entry text // Active path marker - shown right before the entry text
const isOnActivePath = this.activePathIds.has(entry.id); const isOnActivePath = this.activePathIds.has(entry.id);
@ -401,9 +455,8 @@ class TreeList implements Component {
const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : ""; const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
const content = this.getEntryDisplayText(flatNode.node, isSelected); const content = this.getEntryDisplayText(flatNode.node, isSelected);
const suffix = isCurrentLeaf ? theme.fg("accent", " *") : "";
const line = cursor + theme.fg("dim", prefix) + pathMarker + label + content + suffix; const line = cursor + theme.fg("dim", prefix) + pathMarker + label + content;
lines.push(truncateToWidth(line, width)); lines.push(truncateToWidth(line, width));
} }
@ -437,12 +490,12 @@ class TreeList implements Component {
if (textContent) { if (textContent) {
result = theme.fg("success", "assistant: ") + textContent; result = theme.fg("success", "assistant: ") + textContent;
} else if (msgWithContent.stopReason === "aborted") { } else if (msgWithContent.stopReason === "aborted") {
result = theme.fg("warning", "assistant: ") + theme.fg("muted", "(aborted)"); result = theme.fg("success", "assistant: ") + theme.fg("muted", "(aborted)");
} else if (msgWithContent.errorMessage) { } else if (msgWithContent.errorMessage) {
const errMsg = normalize(msgWithContent.errorMessage).slice(0, 80); const errMsg = normalize(msgWithContent.errorMessage).slice(0, 80);
result = theme.fg("error", "assistant: ") + errMsg; result = theme.fg("success", "assistant: ") + theme.fg("error", errMsg);
} else { } else {
result = theme.fg("muted", "assistant: (no content)"); result = theme.fg("success", "assistant: ") + theme.fg("muted", "(no content)");
} }
} else if (role === "toolResult") { } else if (role === "toolResult") {
const toolMsg = msg as { toolCallId?: string; toolName?: string }; const toolMsg = msg as { toolCallId?: string; toolName?: string };
@ -586,6 +639,12 @@ class TreeList implements Component {
this.selectedIndex = this.selectedIndex === 0 ? this.filteredNodes.length - 1 : this.selectedIndex - 1; this.selectedIndex = this.selectedIndex === 0 ? this.filteredNodes.length - 1 : this.selectedIndex - 1;
} else if (isArrowDown(keyData)) { } else if (isArrowDown(keyData)) {
this.selectedIndex = this.selectedIndex === this.filteredNodes.length - 1 ? 0 : this.selectedIndex + 1; this.selectedIndex = this.selectedIndex === this.filteredNodes.length - 1 ? 0 : this.selectedIndex + 1;
} else if (isArrowLeft(keyData)) {
// Page up
this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisibleLines);
} else if (isArrowRight(keyData)) {
// Page down
this.selectedIndex = Math.min(this.filteredNodes.length - 1, this.selectedIndex + this.maxVisibleLines);
} else if (isEnter(keyData)) { } else if (isEnter(keyData)) {
const selected = this.filteredNodes[this.selectedIndex]; const selected = this.filteredNodes[this.selectedIndex];
if (selected && this.onSelect) { if (selected && this.onSelect) {
@ -719,9 +778,11 @@ export class TreeSelectorComponent extends Container {
this.labelInputContainer = new Container(); this.labelInputContainer = new Container();
this.addChild(new Spacer(1)); this.addChild(new Spacer(1));
this.addChild(new Text(theme.bold("Session Tree"), 1, 0));
this.addChild(new DynamicBorder()); this.addChild(new DynamicBorder());
this.addChild(new TruncatedText(theme.fg("muted", " Type to search. l: label. ^O: cycle filter"), 0, 0)); this.addChild(new Text(theme.bold(" Session Tree"), 1, 0));
this.addChild(
new TruncatedText(theme.fg("muted", " ↑/↓: move. ←/→: page. l: label. ^O: filter. Type to search"), 0, 0),
);
this.addChild(new SearchLine(this.treeList)); this.addChild(new SearchLine(this.treeList));
this.addChild(new DynamicBorder()); this.addChild(new DynamicBorder());
this.addChild(new Spacer(1)); this.addChild(new Spacer(1));

View file

@ -56,6 +56,15 @@ import { UserMessageComponent } from "./components/user-message.js";
import { UserMessageSelectorComponent } from "./components/user-message-selector.js"; import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js"; import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js";
/** Interface for components that can be expanded/collapsed */
interface Expandable {
setExpanded(expanded: boolean): void;
}
function isExpandable(obj: unknown): obj is Expandable {
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
}
export class InteractiveMode { export class InteractiveMode {
private session: AgentSession; private session: AgentSession;
private ui: TUI; private ui: TUI;
@ -1303,13 +1312,7 @@ export class InteractiveMode {
private toggleToolOutputExpansion(): void { private toggleToolOutputExpansion(): void {
this.toolOutputExpanded = !this.toolOutputExpanded; this.toolOutputExpanded = !this.toolOutputExpanded;
for (const child of this.chatContainer.children) { for (const child of this.chatContainer.children) {
if (child instanceof ToolExecutionComponent) { if (isExpandable(child)) {
child.setExpanded(this.toolOutputExpanded);
} else if (child instanceof CompactionSummaryMessageComponent) {
child.setExpanded(this.toolOutputExpanded);
} else if (child instanceof BashExecutionComponent) {
child.setExpanded(this.toolOutputExpanded);
} else if (child instanceof HookMessageComponent) {
child.setExpanded(this.toolOutputExpanded); child.setExpanded(this.toolOutputExpanded);
} }
} }