From 447e9e0b6237bbab513f71a3523101b9c23474f9 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 5 Apr 2026 13:26:50 +0800 Subject: [PATCH] feat: align web conversation folders with drawer design --- src/components/app-ui.tsx | 172 +++++++++++++------------- tests/conversation-home-items.test.ts | 41 +++++- 2 files changed, 125 insertions(+), 88 deletions(-) diff --git a/src/components/app-ui.tsx b/src/components/app-ui.tsx index 4db6b21..318f144 100644 --- a/src/components/app-ui.tsx +++ b/src/components/app-ui.tsx @@ -351,6 +351,18 @@ function riskBadgeColor(level: ConversationItem["riskLevel"]) { } } +export function getConversationListItemPresentation(conversation: ConversationItem) { + const isFolderArchive = conversation.conversationType === "folder_archive"; + return { + href: + isFolderArchive && conversation.folderKey + ? `/conversations/folders/${encodeURIComponent(conversation.folderKey)}` + : `/conversations/${conversation.projectId}`, + title: isFolderArchive ? conversation.threadTitle : conversation.projectTitle, + subtitle: isFolderArchive ? conversation.folderLabel : conversation.deviceNamesPreview.join(" / "), + }; +} + function conversationActionsPath(projectId: string) { return `/api/v1/conversations/${projectId}/actions`; } @@ -411,100 +423,86 @@ export function ConversationList({ }) { return (
- {conversations.map((conversation) => ( -
-
- -
- { + const presentation = getConversationListItemPresentation(conversation); + const isFolderArchive = conversation.conversationType === "folder_archive"; + + return ( +
- -
-
-
-
-
- {conversation.conversationType === "folder_archive" - ? conversation.threadTitle - : conversation.projectTitle} +
+ +
+ + +
+
+
+
+
{presentation.title}
+ {isFolderArchive ? ( + + {conversation.threadCount ?? 0} 个线程 + + ) : ( + + {conversation.riskLevel === "high" + ? "高风险" + : conversation.riskLevel === "medium" + ? "关注" + : "稳定"} + + )} + {conversation.unreadCount > 0 ? ( + + {conversation.unreadCount} + + ) : null}
- {conversation.conversationType === "folder_archive" ? ( - - {conversation.threadCount ?? 0} 个线程 - - ) : ( - - {conversation.riskLevel === "high" - ? "高风险" - : conversation.riskLevel === "medium" - ? "关注" - : "稳定"} - - )} - {conversation.unreadCount > 0 ? ( - - {conversation.unreadCount} - - ) : null} +
{presentation.subtitle}
+
{conversation.preview}
-
- {conversation.conversationType === "folder_archive" - ? conversation.folderLabel - : conversation.deviceNamesPreview.join(" / ")} -
-
- {conversation.preview} -
-
-
-
- {conversation.projectId === "master-agent" - ? "置顶" - : conversation.manualPinned +
+
+ {conversation.projectId === "master-agent" ? "置顶" - : ""} -
-
- {conversation.latestReplyLabel} -
- {conversation.contextBudgetIndicator.visible && - conversation.contextBudgetIndicator.percent !== undefined ? ( - - ) : ( -
- {conversation.activeDeviceCount > 1 - ? `${conversation.activeDeviceCount} 台协作` - : ""} + : conversation.manualPinned + ? "置顶" + : ""}
- )} +
{conversation.latestReplyLabel}
+ {conversation.contextBudgetIndicator.visible && + conversation.contextBudgetIndicator.percent !== undefined ? ( + + ) : ( +
+ {conversation.activeDeviceCount > 1 + ? `${conversation.activeDeviceCount} 台协作` + : ""} +
+ )} +
-
- -
- ))} + +
+ ); + })}
); } diff --git a/tests/conversation-home-items.test.ts b/tests/conversation-home-items.test.ts index 0f0482c..a2c3295 100644 --- a/tests/conversation-home-items.test.ts +++ b/tests/conversation-home-items.test.ts @@ -9,6 +9,7 @@ let readState: (typeof import("../src/lib/boss-data"))["readState"]; let getConversationHomeItems: (typeof import("../src/lib/boss-projections"))["getConversationHomeItems"]; let getConversationFolderView: (typeof import("../src/lib/boss-projections"))["getConversationFolderView"]; let formatTimestampLabel: (typeof import("../src/lib/boss-projections"))["formatTimestampLabel"]; +let getConversationListItemPresentation: (typeof import("../src/components/app-ui"))["getConversationListItemPresentation"]; async function setup() { if (runtimeRoot) return; @@ -16,14 +17,16 @@ async function setup() { process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); - const [data, projections] = await Promise.all([ + const [data, projections, ui] = await Promise.all([ import("../src/lib/boss-data.ts"), import("../src/lib/boss-projections.ts"), + import("../src/components/app-ui.tsx"), ]); readState = data.readState; getConversationHomeItems = projections.getConversationHomeItems; getConversationFolderView = projections.getConversationFolderView; formatTimestampLabel = projections.formatTimestampLabel; + getConversationListItemPresentation = ui.getConversationListItemPresentation; } test.after(async () => { @@ -392,6 +395,42 @@ test("conversation home groups multiple imported threads by folder while keeping ); }); +test("folder archive homepage rows keep the project title, compact subtitle, and folder route", async () => { + await setup(); + const state = await readState(); + + state.projects = state.projects.filter((project) => project.id === "master-agent"); + state.projects.push( + buildImportedThreadProject( + "mac-studio", + "boss-thread-1", + "Boss", + "boss", + "归档确认", + "thread-1", + "2026-03-30T11:00:00+08:00", + ), + buildImportedThreadProject( + "mac-studio", + "boss-thread-2", + "Boss", + "boss", + "发布回滚", + "thread-2", + "2026-03-30T12:00:00+08:00", + ), + ); + + const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive"); + + assert.ok(folder, "expected grouped folder archive item"); + const presentation = getConversationListItemPresentation(folder!); + + assert.equal(presentation.title, "Boss"); + assert.equal(presentation.subtitle, "2 个线程 · 最近:发布回滚"); + assert.equal(presentation.href, "/conversations/folders/mac-studio%3Aboss"); +}); + test("conversation items expose context status while keeping idle activity silent", async () => { await setup(); const state = await readState();