fix(frontend): read web_fetch titles that start with blank lines or indented headings (#5560)

* fix(frontend): read web_fetch titles that start with blank lines or indented headings

* docs(frontend): describe the indented-code guard as it actually behaves

* fix(frontend): reject mixed code indentation in web-fetch titles

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
lihongyuan99 2026-09-20 20:26:48 +08:00 committed by GitHub
parent c52ad191f4
commit 1e3bfa09d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 78 additions and 7 deletions

View File

@ -1128,6 +1128,9 @@ empty list is forwarded and imposes no restriction of that kind. See the
When using Tavily for `web_fetch`, extracted pages without a title use their URL
as the heading; their content remains available to the agent.
Chat tool-step titles accept leading blank lines and up to three spaces before
a page's first H1 heading. Indented code, including mixed spaces and tabs, is
not used as a title; the tool step falls back to the URL.
Tavily search and fetch each read `api_key` from their own tool entry in
`config.yaml`, falling back to `TAVILY_API_KEY` when omitted. Fetch does not
reuse the search entry's key, so search can use a different provider. If you

View File

@ -83,6 +83,11 @@ More specific `AGENTS.md` files under `src/` contain the frontend sections split
## Code Style
`core/utils/markdown.ts` reads web-fetch titles from the first nonblank line.
Match zero to three literal spaces before `# ` without trimming indentation;
mixed space/tab code blocks must fall back to the URL. Keep this local to title
extraction rather than changing the shared streamdown fence parser.
Custom Agent `display_name` is an optional Unicode UI label, edited in
`AgentSettingsDialog`. Use it with a fallback to `name` for gallery/chat text;
keep `name` for React identity, URLs, requests, and runtime `agent_name`.

View File

@ -1,10 +1,13 @@
// Converter output can start with blank lines, and CommonMark allows up to three
// literal spaces before an ATX heading. Do not trim code indentation into a title.
export function extractTitleFromMarkdown(markdown: string) {
if (markdown.startsWith("# ")) {
let title = markdown.split("\n")[0]!.trim();
if (title.startsWith("# ")) {
title = title.slice(2).trim();
}
return title;
const firstLine = markdown.split("\n").find((line) => line.trim() !== "");
if (firstLine === undefined) {
return undefined;
}
return undefined;
const headingPrefix = /^ {0,3}# /.exec(firstLine);
if (!headingPrefix) {
return undefined;
}
return firstLine.slice(headingPrefix[0].length).trim() || undefined;
}

View File

@ -0,0 +1,60 @@
import { expect, test } from "@rstest/core";
import { extractTitleFromMarkdown } from "@/core/utils/markdown";
test("reads the title from a leading ATX heading", () => {
expect(extractTitleFromMarkdown("# Real Title\n\nbody")).toBe("Real Title");
});
test("skips blank lines before the first heading", () => {
expect(extractTitleFromMarkdown("\n# Real Title\n\nbody")).toBe("Real Title");
expect(extractTitleFromMarkdown(" \n\n# Real Title")).toBe("Real Title");
});
test("accepts the up-to-three-space indentation CommonMark allows", () => {
expect(extractTitleFromMarkdown(" # Real Title")).toBe("Real Title");
});
test("ignores an indented code block that starts with a hash", () => {
expect(extractTitleFromMarkdown(" # Not A Title")).toBeUndefined();
expect(extractTitleFromMarkdown("\t# Not A Title")).toBeUndefined();
});
test.each([" \t", " \t", " \t"])(
"does not treat mixed indentation %j as a heading",
(indent) => {
expect(
extractTitleFromMarkdown(`${indent}# Code comment\n# Later heading`),
).toBeUndefined();
expect(
extractTitleFromMarkdown(`\n \n${indent}# Code comment`),
).toBeUndefined();
},
);
test.each(["", " ", " ", " "])(
"accepts a heading with %j indentation and CRLF line endings",
(indent) => {
expect(extractTitleFromMarkdown(`\r\n${indent}# Real Title\r\nbody`)).toBe(
"Real Title",
);
},
);
test("ignores headings that are not level 1", () => {
expect(extractTitleFromMarkdown("## Section")).toBeUndefined();
expect(extractTitleFromMarkdown("#NoSpace")).toBeUndefined();
});
test("does not report an empty heading as a title", () => {
expect(extractTitleFromMarkdown("# \n\nbody")).toBeUndefined();
expect(extractTitleFromMarkdown("#")).toBeUndefined();
});
test("returns undefined when the document has no content", () => {
expect(extractTitleFromMarkdown("")).toBeUndefined();
expect(extractTitleFromMarkdown(" \n ")).toBeUndefined();
expect(
extractTitleFromMarkdown("Plain text with no heading"),
).toBeUndefined();
});