mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 09:02:08 +00:00
feat(coding-agent): improve tree selector tool display
- Tool results show formatted call info like [read: path:1-10], [bash: cmd] - Skip assistant messages with stopReason error/aborted - Skip assistant messages with only tool calls (no text) - Format built-in tools like tool-execution.ts (read, write, edit, bash, grep, find, ls) - Custom tools show truncated JSON args
This commit is contained in:
parent
b153b528e8
commit
2922020382
1 changed files with 118 additions and 7 deletions
|
|
@ -35,6 +35,12 @@ type FilterMode = "default" | "user-only" | "labeled-only" | "all";
|
||||||
/**
|
/**
|
||||||
* Tree list component with selection and ASCII art visualization
|
* Tree list component with selection and ASCII art visualization
|
||||||
*/
|
*/
|
||||||
|
/** Tool call info for lookup */
|
||||||
|
interface ToolCallInfo {
|
||||||
|
name: string;
|
||||||
|
arguments: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
class TreeList implements Component {
|
class TreeList implements Component {
|
||||||
private flatNodes: FlatNode[] = [];
|
private flatNodes: FlatNode[] = [];
|
||||||
private filteredNodes: FlatNode[] = [];
|
private filteredNodes: FlatNode[] = [];
|
||||||
|
|
@ -43,6 +49,7 @@ class TreeList implements Component {
|
||||||
private maxVisibleLines: number;
|
private maxVisibleLines: number;
|
||||||
private filterMode: FilterMode = "default";
|
private filterMode: FilterMode = "default";
|
||||||
private searchQuery = "";
|
private searchQuery = "";
|
||||||
|
private toolCallMap: Map<string, ToolCallInfo> = new Map();
|
||||||
|
|
||||||
public onSelect?: (entryId: string) => void;
|
public onSelect?: (entryId: string) => void;
|
||||||
public onCancel?: () => void;
|
public onCancel?: () => void;
|
||||||
|
|
@ -65,6 +72,7 @@ class TreeList implements Component {
|
||||||
|
|
||||||
private flattenTree(roots: SessionTreeNode[]): FlatNode[] {
|
private flattenTree(roots: SessionTreeNode[]): FlatNode[] {
|
||||||
const result: FlatNode[] = [];
|
const result: FlatNode[] = [];
|
||||||
|
this.toolCallMap.clear();
|
||||||
|
|
||||||
// Use iterative approach to avoid stack overflow on deep trees
|
// Use iterative approach to avoid stack overflow on deep trees
|
||||||
// Stack items: [node, prefix, isLast, showConnector]
|
// Stack items: [node, prefix, isLast, showConnector]
|
||||||
|
|
@ -79,6 +87,20 @@ class TreeList implements Component {
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
const [node, prefix, isLast, showConnector] = stack.pop()!;
|
const [node, prefix, isLast, showConnector] = stack.pop()!;
|
||||||
|
|
||||||
|
// Extract tool calls from assistant messages for later lookup
|
||||||
|
const entry = node.entry;
|
||||||
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
||||||
|
const content = (entry.message as { content?: unknown }).content;
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
for (const block of content) {
|
||||||
|
if (typeof block === "object" && block !== null && "type" in block && block.type === "toolCall") {
|
||||||
|
const tc = block as { id: string; name: string; arguments: Record<string, unknown> };
|
||||||
|
this.toolCallMap.set(tc.id, { name: tc.name, arguments: tc.arguments });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const depth = prefix.length / 3 + (showConnector ? 1 : 0);
|
const depth = prefix.length / 3 + (showConnector ? 1 : 0);
|
||||||
result.push({ node, depth, isLast, prefix, showConnector });
|
result.push({ node, depth, isLast, prefix, showConnector });
|
||||||
|
|
||||||
|
|
@ -115,7 +137,21 @@ class TreeList implements Component {
|
||||||
this.filteredNodes = this.flatNodes.filter((flatNode) => {
|
this.filteredNodes = this.flatNodes.filter((flatNode) => {
|
||||||
const entry = flatNode.node.entry;
|
const entry = flatNode.node.entry;
|
||||||
|
|
||||||
// Apply filter mode first
|
// Skip assistant messages with failed stopReason or no text content
|
||||||
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
||||||
|
const msg = entry.message as { stopReason?: string; content?: unknown };
|
||||||
|
// Skip aborted/error messages
|
||||||
|
if (msg.stopReason && msg.stopReason !== "stop" && msg.stopReason !== "toolUse") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Skip messages with only tool calls (no text content)
|
||||||
|
const hasText = this.hasTextContent(msg.content);
|
||||||
|
if (!hasText) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filter mode
|
||||||
let passesFilter = true;
|
let passesFilter = true;
|
||||||
if (this.filterMode === "user-only") {
|
if (this.filterMode === "user-only") {
|
||||||
passesFilter =
|
passesFilter =
|
||||||
|
|
@ -296,15 +332,23 @@ class TreeList implements Component {
|
||||||
case "message": {
|
case "message": {
|
||||||
const msg = entry.message;
|
const msg = entry.message;
|
||||||
const role = msg.role;
|
const role = msg.role;
|
||||||
if (role === "user" || role === "assistant") {
|
if (role === "user") {
|
||||||
const msgWithContent = msg as { content?: unknown };
|
const msgWithContent = msg as { content?: unknown };
|
||||||
const content = normalize(this.extractContent(msgWithContent.content));
|
const content = normalize(this.extractContent(msgWithContent.content));
|
||||||
const roleColor = role === "user" ? "accent" : "success";
|
result = theme.fg("accent", "user: ") + content;
|
||||||
result = theme.fg(roleColor, `${role}: `) + content;
|
} else if (role === "assistant") {
|
||||||
|
// Assistant messages without text content are filtered out, so we always have text here
|
||||||
|
const msgWithContent = msg as { content?: unknown };
|
||||||
|
const textContent = normalize(this.extractContent(msgWithContent.content));
|
||||||
|
result = theme.fg("success", "assistant: ") + textContent;
|
||||||
} else if (role === "toolResult") {
|
} else if (role === "toolResult") {
|
||||||
const toolMsg = msg as { toolName?: string };
|
const toolMsg = msg as { toolCallId?: string; toolName?: string };
|
||||||
const toolName = toolMsg.toolName ?? "tool";
|
const toolCall = toolMsg.toolCallId ? this.toolCallMap.get(toolMsg.toolCallId) : undefined;
|
||||||
result = theme.fg("muted", `[${toolName}]`);
|
if (toolCall) {
|
||||||
|
result = theme.fg("muted", this.formatToolCall(toolCall.name, toolCall.arguments));
|
||||||
|
} else {
|
||||||
|
result = theme.fg("muted", `[${toolMsg.toolName ?? "tool"}]`);
|
||||||
|
}
|
||||||
} else if (role === "bashExecution") {
|
} else if (role === "bashExecution") {
|
||||||
const bashMsg = msg as { command?: string };
|
const bashMsg = msg as { command?: string };
|
||||||
result = theme.fg("dim", `[bash]: ${normalize(bashMsg.command ?? "")}`);
|
result = theme.fg("dim", `[bash]: ${normalize(bashMsg.command ?? "")}`);
|
||||||
|
|
@ -367,6 +411,73 @@ class TreeList implements Component {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasTextContent(content: unknown): boolean {
|
||||||
|
if (typeof content === "string") return content.trim().length > 0;
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
for (const c of content) {
|
||||||
|
if (typeof c === "object" && c !== null && "type" in c && c.type === "text") {
|
||||||
|
const text = (c as { text?: string }).text;
|
||||||
|
if (text && text.trim().length > 0) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatToolCall(name: string, args: Record<string, unknown>): string {
|
||||||
|
const shortenPath = (p: string): string => {
|
||||||
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
||||||
|
if (home && p.startsWith(home)) return `~${p.slice(home.length)}`;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case "read": {
|
||||||
|
const path = shortenPath(String(args.path || args.file_path || ""));
|
||||||
|
const offset = args.offset as number | undefined;
|
||||||
|
const limit = args.limit as number | undefined;
|
||||||
|
let display = path;
|
||||||
|
if (offset !== undefined || limit !== undefined) {
|
||||||
|
const start = offset ?? 1;
|
||||||
|
const end = limit !== undefined ? start + limit - 1 : "";
|
||||||
|
display += `:${start}${end ? `-${end}` : ""}`;
|
||||||
|
}
|
||||||
|
return `[read: ${display}]`;
|
||||||
|
}
|
||||||
|
case "write": {
|
||||||
|
const path = shortenPath(String(args.path || args.file_path || ""));
|
||||||
|
return `[write: ${path}]`;
|
||||||
|
}
|
||||||
|
case "edit": {
|
||||||
|
const path = shortenPath(String(args.path || args.file_path || ""));
|
||||||
|
return `[edit: ${path}]`;
|
||||||
|
}
|
||||||
|
case "bash": {
|
||||||
|
const cmd = String(args.command || "").slice(0, 50);
|
||||||
|
return `[bash: ${cmd}${(args.command as string)?.length > 50 ? "..." : ""}]`;
|
||||||
|
}
|
||||||
|
case "grep": {
|
||||||
|
const pattern = String(args.pattern || "");
|
||||||
|
const path = shortenPath(String(args.path || "."));
|
||||||
|
return `[grep: /${pattern}/ in ${path}]`;
|
||||||
|
}
|
||||||
|
case "find": {
|
||||||
|
const pattern = String(args.pattern || "");
|
||||||
|
const path = shortenPath(String(args.path || "."));
|
||||||
|
return `[find: ${pattern} in ${path}]`;
|
||||||
|
}
|
||||||
|
case "ls": {
|
||||||
|
const path = shortenPath(String(args.path || "."));
|
||||||
|
return `[ls: ${path}]`;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// Custom tool - show name and truncated JSON args
|
||||||
|
const argsStr = JSON.stringify(args).slice(0, 40);
|
||||||
|
return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? "..." : ""}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleInput(keyData: string): void {
|
handleInput(keyData: string): void {
|
||||||
if (isArrowUp(keyData)) {
|
if (isArrowUp(keyData)) {
|
||||||
this.selectedIndex = this.selectedIndex === 0 ? this.filteredNodes.length - 1 : this.selectedIndex - 1;
|
this.selectedIndex = this.selectedIndex === 0 ? this.filteredNodes.length - 1 : this.selectedIndex - 1;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue