From 59aa91d6223db25e51da41a90dce076f32bc542d Mon Sep 17 00:00:00 2001 From: Hyeonsang Cho Date: Sat, 26 Sep 2026 13:06:12 +0900 Subject: [PATCH] fix(projects): label untitled threads in the conversation-files view (#5879) * fix(projects): label untitled threads in the conversation-files view GET /api/projects/{id}/threads/files returns each member thread's display_name as `str | None`: the thread meta row is created with NULL and only filled once title generation has run after the first reply, or never if that step fails. The frontend's ProjectThreadFileGroup declared the field as a required string and ConversationFileGroup rendered it verbatim, so a thread whose files were uploaded before its first reply showed them under an empty heading. The sibling ProjectThread type already models the field as nullable and every consumer of it falls back to t.projects.untitled. Make the group type nullable to match the wire, and render the heading through a shared threadTitle() helper that both the thread-name map and the file group now use, so the "Untitled" rule lives in one place. The DOM test covers null, missing and whitespace-only titles. * docs(changelog): reference #5879 in the conversation-files entries --------- Co-authored-by: Willem Jiang --- CHANGELOG.md | 7 ++++ CHANGELOG_zh.md | 5 +++ .../projects/project-documents-section.tsx | 14 ++++++-- frontend/src/core/projects/types.ts | 6 +++- .../project-documents-section.dom.test.tsx | 35 +++++++++++++++++++ 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c41262b6..3950969def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -323,6 +323,12 @@ This release closes that milestone with **181 merged pull requests**. ### Fixed +- **projects:** The conversation-files view no longer shows an empty heading + for a member thread that has no title yet. A thread's `display_name` is + `null` on the wire until title generation has run (or if it never does), but + the file-group type declared it as a required string and rendered it as-is, + so files uploaded before the first reply sat under a blank line. Such groups + now read "Untitled", matching the project's thread list. ([#5879]) - **skills:** Deleting a custom skill whose package directory is an operator-managed symlink now succeeds. The storage contract accepts a one-level link under `custom/`, but `delete_custom_skill` removed the @@ -6102,5 +6108,6 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5855]: https://github.com/bytedance/deer-flow/pull/5855 [#5856]: https://github.com/bytedance/deer-flow/pull/5856 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 +[#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index ec525152e9..de7334c425 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -328,6 +328,10 @@ ### 修复 +- **项目:** 会话文件视图不再为尚无标题的成员会话显示空标题。会话的 `display_name` 在标题 + 生成运行之前(或从未运行时)在接口上为 `null`,但文件分组类型将其声明为必填字符串并原样 + 渲染,因此在首次回复之前上传的文件会挂在一行空白之下。这类分组现在显示为“未命名”,与 + 项目的会话列表一致。([#5879]) - **技能:** 删除包目录为运维方管理的符号链接的自定义技能现在可以成功。存储契约允许 `custom/` 下的一级链接,但 `delete_custom_skill` 用 `shutil.rmtree` 删除包目录,而它拒绝 符号链接:删除在写入历史记录之后以 `OSError` 失败,退出时清空了用户的技能投影视图, @@ -5192,4 +5196,5 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5855]: https://github.com/bytedance/deer-flow/pull/5855 [#5856]: https://github.com/bytedance/deer-flow/pull/5856 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 +[#5879]: https://github.com/bytedance/deer-flow/pull/5879 [#5881]: https://github.com/bytedance/deer-flow/pull/5881 diff --git a/frontend/src/components/workspace/projects/project-documents-section.tsx b/frontend/src/components/workspace/projects/project-documents-section.tsx index 8a77d1092c..43e09f22b9 100644 --- a/frontend/src/components/workspace/projects/project-documents-section.tsx +++ b/frontend/src/components/workspace/projects/project-documents-section.tsx @@ -73,6 +73,14 @@ import { getFileIcon } from "@/core/utils/files"; import { env } from "@/env"; import { cn } from "@/lib/utils"; +/** A thread's wire title, or the localized "Untitled" while it has none. */ +function threadTitle( + displayName: string | null | undefined, + untitled: string, +): string { + return displayName?.trim() ? displayName : untitled; +} + function errorToastMessage(error: unknown, fallback: string): string { return error instanceof Error && error.message ? error.message : fallback; } @@ -143,7 +151,7 @@ function ProjectDocumentShelf({ const threadNameById = new Map( threads.map((thread) => [ thread.thread_id, - thread.display_name?.trim() ? thread.display_name : t.projects.untitled, + threadTitle(thread.display_name, t.projects.untitled), ]), ); @@ -854,7 +862,9 @@ function ConversationFileGroup({ return (
-
{group.display_name}
+
+ {threadTitle(group.display_name, t.projects.untitled)} +
    {group.files.map((file) => { const previewUrl = resolveArtifactOpenURL({ diff --git a/frontend/src/core/projects/types.ts b/frontend/src/core/projects/types.ts index c81ac25ee2..10431a602e 100644 --- a/frontend/src/core/projects/types.ts +++ b/frontend/src/core/projects/types.ts @@ -100,7 +100,11 @@ export type ProjectThreadFile = { /** One member-thread group of the conversation-files view (spec §7.4). */ export type ProjectThreadFileGroup = { thread_id: string; - display_name: string; + /** + * The member thread's title. Nullable on the wire like ``ProjectThread``: + * the thread meta row starts as ``null`` and is filled by title generation. + */ + display_name?: string | null; updated_at: string; files: ProjectThreadFile[]; /** True when the thread's files were cut at the request's file_limit. */ diff --git a/frontend/tests/unit/components/workspace/projects/project-documents-section.dom.test.tsx b/frontend/tests/unit/components/workspace/projects/project-documents-section.dom.test.tsx index 61481fdcd8..e671cc6620 100644 --- a/frontend/tests/unit/components/workspace/projects/project-documents-section.dom.test.tsx +++ b/frontend/tests/unit/components/workspace/projects/project-documents-section.dom.test.tsx @@ -679,4 +679,39 @@ describe("ProjectDocumentsSection", () => { expect(screen.getByText("chart.png")).toBeDefined(); expect(screen.queryByTestId("thread-files-truncated")).toBeNull(); }); + + it.each([ + ["null", null], + ["missing", undefined], + ["blank", " "], + ])( + "labels a thread group whose title is %s as Untitled instead of rendering an empty heading", + (_label, displayName) => { + // ``display_name`` is nullable on the wire: the thread meta row starts + // as ``null`` and is only filled once title generation has run. + mocks.threadFileGroups = [ + { + thread_id: "thread-9", + display_name: displayName, + updated_at: "2026-09-09T00:00:00Z", + truncated: false, + files: [ + { + kind: "output", + name: "chart.png", + size_bytes: 1024, + modified_at: "2026-09-09T00:00:00Z", + artifact_url: + "/api/threads/thread-9/artifacts/mnt/user-data/outputs/chart.png", + }, + ], + }, + ]; + render( + , + { wrapper: Wrapper }, + ); + expect(screen.getByText("Untitled")).toBeDefined(); + }, + ); });