fix: normalize dashboard label rendering (#1732)

* fix: normalize tool call rendering

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: simplify tool call labels

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: capitalize dashboard UI labels

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
(cherry picked from commit d714586ce6928e4848214a8fce78cc4597ff2684)

Co-authored-by: Ramon Nogueira <ramon.nogueira@langchain.dev>
This commit is contained in:
Adam Moussa 2026-07-17 16:22:38 -04:00
parent ead210927d
commit 58941d1edd
No known key found for this signature in database
9 changed files with 149 additions and 75 deletions

View file

@ -61,6 +61,10 @@ function triToBool(value: TriState): boolean | undefined {
return undefined
}
function displayStatus(status: AgentStatus): string {
return STATUS_OPTIONS.find((option) => option.value === status)?.label ?? status
}
export function AgentsThreadsPage({
filters,
onFiltersChange,
@ -279,8 +283,8 @@ function ThreadListItem({ thread }: { thread: AgentThread }) {
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-[var(--ui-text)]">{thread.title}</p>
<p className="truncate text-[11px] text-[var(--ui-text-dim)]">
{thread.repoFullName || "no repo"} · {thread.status}
{isResolved ? " · resolved" : ""}
{thread.repoFullName || "No repo"} · {displayStatus(thread.status)}
{isResolved ? " · Resolved" : ""}
</p>
</div>
<button

View file

@ -75,11 +75,11 @@ export function WorkflowApprovalCard({
</div>
<p className="text-xs text-[var(--ui-text-dim)]">
{approval.repo || "Repository"} on{" "}
{approval.branch || "current branch"} ·{" "}
{approval.branch || "Current branch"} ·{" "}
{shortSha(approval.baseSha)} → {shortSha(approval.headSha)}
</p>
<p className="font-mono text-[0.68rem] break-all text-[var(--ui-text-dim)]">
fingerprint: {approval.fingerprint}
Fingerprint: {approval.fingerprint}
</p>
</div>
<div className="flex shrink-0 flex-wrap gap-2">

View file

@ -393,7 +393,7 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
>
<img
src={`data:${image.mimeType};base64,${image.base64}`}
alt={image.fileName || "pending image"}
alt={image.fileName || "Pending image"}
className="size-16 rounded-lg border border-[var(--ui-border)] object-cover"
/>
<button

View file

@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { formatToolDisplay } from "./toolExecutionDisplay";
describe("formatToolDisplay", () => {
const projectPath = "/workspace/open-swe";
it("renders read_file with the file_path alias consistently", () => {
expect(
formatToolDisplay(
"read_file /workspace/open-swe/AGENTS.md",
"read",
{ file_path: "/workspace/open-swe/AGENTS.md" },
projectPath,
),
).toBe("Read AGENTS.md");
});
it("renders ls as a list operation with a relative path", () => {
expect(
formatToolDisplay(
"ls /workspace/open-swe/ui",
"read",
{ path: "/workspace/open-swe/ui" },
projectPath,
),
).toBe("List ui");
});
it("renders search tools with their pattern", () => {
expect(
formatToolDisplay("grep", "search", { pattern: "tool_calls" }, projectPath),
).toBe('Search "tool_calls"');
});
it("normalizes write_todos", () => {
expect(formatToolDisplay("write todos", "other", {}, projectPath)).toBe("Update todos");
});
});

View file

@ -1,7 +1,8 @@
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
import { MultiFileDiff } from "@pierre/diffs/react";
import { DiffView } from "./DiffView";
import type { AcpToolKind, ToolExecutionChunk } from "@/features/agents/lib/types";
import { formatToolDisplay } from "./toolExecutionDisplay";
import type { ToolExecutionChunk } from "@/features/agents/lib/types";
import { useDiffOptions } from "@/features/agents/utils/diffUtils";
import { countLineChanges } from "@/features/agents/utils/diffStats";
@ -27,60 +28,6 @@ function getFileName(path: string): string {
return parts[parts.length - 1] || path;
}
function formatToolDisplay(
title: string,
toolKind: AcpToolKind,
input: Record<string, unknown> | undefined,
projectPath?: string,
): string {
const path = input?.path as string | undefined;
const pattern = input?.pattern as string | undefined;
const query = input?.query as string | undefined;
const url = input?.url as string | undefined;
const command = input?.command as string | undefined;
switch (toolKind) {
case "read": {
if (path) {
const displayPath = stripProjectPath(path, projectPath);
return `Read(${displayPath})`;
}
return title;
}
case "search": {
if (pattern) {
const truncated = pattern.length > 40 ? pattern.slice(0, 40) + "..." : pattern;
return `Search("${truncated}")`;
}
if (query) {
return `Search("${query.slice(0, 40)}${query.length > 40 ? "..." : ""}")`;
}
return title;
}
case "fetch": {
if (url) {
return `Fetch(${url.slice(0, 50)}${url.length > 50 ? "..." : ""})`;
}
return title;
}
case "execute": {
if (command) {
const truncated = command.length > 60 ? command.slice(0, 60) + "..." : command;
return `Shell(${truncated})`;
}
return title;
}
case "edit":
case "delete":
case "move":
return title;
case "think":
return "Thinking...";
default:
return title;
}
}
const InlineDiffCollapsible = memo(function InlineDiffCollapsible({
filePath,
fileName,

View file

@ -0,0 +1,82 @@
import type { AcpToolKind } from "@/features/agents/lib/types";
function stripProjectPath(path: string, projectPath?: string): string {
if (!projectPath || !path.startsWith(projectPath)) return path;
const relative = path.slice(projectPath.length);
return relative.replace(/^\/+/, "") || ".";
}
function firstStringArg(
input: Record<string, unknown> | undefined,
keys: Array<string>,
): string | undefined {
if (!input) return undefined;
for (const key of keys) {
const value = input[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return undefined;
}
function truncateMiddle(value: string, maxLength: number): string {
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
}
function normalizedToolName(title: string): string {
return title.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "";
}
function humanizeToolTitle(title: string): string {
return title.replace(/_/g, " ").trim() || "Tool";
}
export function formatToolDisplay(
title: string,
toolKind: AcpToolKind,
input: Record<string, unknown> | undefined,
projectPath?: string,
): string {
const toolName = normalizedToolName(title);
const path = firstStringArg(input, ["path", "file_path", "target_file"]);
const pattern = firstStringArg(input, ["pattern"]);
const query = firstStringArg(input, ["query"]);
const url = firstStringArg(input, ["url"]);
const command = firstStringArg(input, ["command"]);
switch (toolKind) {
case "read": {
if (path) {
const displayPath = stripProjectPath(path, projectPath);
return toolName === "ls" ? `List ${displayPath}` : `Read ${displayPath}`;
}
return humanizeToolTitle(title);
}
case "search": {
if (pattern) return `Search "${truncateMiddle(pattern, 40)}"`;
if (query) return `Search "${truncateMiddle(query, 40)}"`;
if (path) return `Search ${stripProjectPath(path, projectPath)}`;
return humanizeToolTitle(title);
}
case "fetch": {
if (url) return `Fetch ${truncateMiddle(url, 50)}`;
return humanizeToolTitle(title);
}
case "execute": {
if (command) return `Shell ${truncateMiddle(command, 60)}`;
return humanizeToolTitle(title);
}
case "edit":
case "delete":
case "move":
return humanizeToolTitle(title);
case "think":
return "Thinking...";
default: {
if (toolName === "write_todos" || title.toLowerCase().startsWith("write todos")) {
return "Update todos";
}
if (toolName === "ls" && path) return `List ${stripProjectPath(path, projectPath)}`;
return humanizeToolTitle(title);
}
}
}

View file

@ -3,17 +3,17 @@ import { useEffect, useRef, useState } from "react";
import { formatElapsed } from "@/lib/utils";
const BUSY_TEXTS: Array<{ present: string; past: string }> = [
{ present: "vibing...", past: "Vibed" },
{ present: "noodling...", past: "Noodled" },
{ present: "pondering...", past: "Pondered" },
{ present: "thinking really hard...", past: "Thought really hard" },
{ present: "spinning up...", past: "Spun up" },
{ present: "connecting the dots...", past: "Connected the dots" },
{ present: "brewing ideas...", past: "Brewed ideas" },
{ present: "cooking...", past: "Cooked" },
{ present: "crunching...", past: "Crunched" },
{ present: "scheming...", past: "Schemed" },
{ present: "processing...", past: "Processed" },
{ present: "Vibing...", past: "Vibed" },
{ present: "Noodling...", past: "Noodled" },
{ present: "Pondering...", past: "Pondered" },
{ present: "Thinking really hard...", past: "Thought really hard" },
{ present: "Spinning up...", past: "Spun up" },
{ present: "Connecting the dots...", past: "Connected the dots" },
{ present: "Brewing ideas...", past: "Brewed ideas" },
{ present: "Cooking...", past: "Cooked" },
{ present: "Crunching...", past: "Crunched" },
{ present: "Scheming...", past: "Schemed" },
{ present: "Processing...", past: "Processed" },
];
const THINKING_SETTLE_MS = 300;

View file

@ -528,7 +528,7 @@ export const PromptBar = memo(function PromptBar({
<div key={i} className="relative group">
<img
src={`data:${img.mimeType};base64,${img.base64}`}
alt={img.fileName || "pending image"}
alt={img.fileName || "Pending image"}
className="w-16 h-16 object-cover rounded border border-gray-600"
/>
<button

View file

@ -5,10 +5,11 @@ import type { AssembledToolCall, SubagentDiscoverySnapshot } from "@langchain/re
import type { Chunk, DiffData, Message, ToolExecutionChunk } from "./types";
const READ_TOOLS = new Set(["read_file", "read", "glob", "grep"]);
const READ_TOOLS = new Set(["read_file", "read", "ls"]);
const EDIT_TOOLS = new Set(["write_file", "edit_file", "str_replace", "write", "edit", "patch"]);
const EXECUTE_TOOLS = new Set(["execute", "bash", "shell", "run_terminal_cmd"]);
const SEARCH_TOOLS = new Set(["glob", "grep", "web_search", "fetch_url", "search"]);
const SEARCH_TOOLS = new Set(["glob", "grep", "web_search", "search"]);
const FETCH_TOOLS = new Set(["fetch", "fetch_url", "http_request"]);
const INTERNAL_TOOLS = new Set(["confirming_completion", "no_op"]);
type ToolKind = ToolExecutionChunk["toolKind"];
@ -19,14 +20,15 @@ function toolKind(name: string): ToolKind {
if (lowered === "task") return "task";
if (lowered === "slack_thread_reply") return "slack";
if (lowered === "linear_comment") return "linear";
if (lowered === "write_todos") return "other";
if (EDIT_TOOLS.has(lowered) || ["edit", "write", "replace"].some((t) => lowered.includes(t))) {
return "edit";
}
if (EXECUTE_TOOLS.has(lowered)) return "execute";
if (FETCH_TOOLS.has(lowered)) return "fetch";
if (SEARCH_TOOLS.has(lowered)) return "search";
if (READ_TOOLS.has(lowered) || lowered.includes("read")) return "read";
if (lowered === "think") return "think";
if (["fetch", "fetch_url", "http_request"].includes(lowered)) return "fetch";
return "other";
}