# 会话首页项目文件夹与抽屉模式 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 让 Boss 首页稳定呈现“单线程项目直显、2 个及以上线程项目显示为项目文件夹”的微信式会话结构,并保证搜索、置顶、排序、文件夹页和前台渲染都一致。 **Architecture:** 基于现有 `folder_archive` 半实现继续收口,而不是推翻重做。服务端投影负责把会话项稳定聚合成“单线程/项目文件夹”两种模型,Web 与 Android 首页只消费聚合后的首页项;搜索继续保留线程可达性,但点击多线程项目中的线程时仍先回到项目文件夹语义。 **Tech Stack:** Next.js App Router、TypeScript、文件型状态存储 `data/boss-state.json`、Android 原生客户端、Node test runner、Gradle unit tests --- ## 文件结构 ### 服务端首页投影与文件夹详情 - Modify: `/Users/kris/code/boss/src/lib/boss-projections.ts` - Test: `/Users/kris/code/boss/tests/conversation-home-items.test.ts` 职责: - 收紧 `getConversationHomeItems()` 聚合规则 - 明确文件夹项副标题、时间、预览、上下文环和活动状态的来源 - 保证 `1 ↔ 2+` 线程数变化时首页自动升降级 - 保持 `getConversationFolderView()` 与首页聚合一致 ### Web 会话首页与搜索 - Modify: `/Users/kris/code/boss/src/components/app-ui.tsx` - Test: `/Users/kris/code/boss/tests/conversation-home-items.test.ts` 职责: - 首页渲染项目文件夹会话项 - 搜索保留线程可达性,但不打破文件夹心智 - 文件夹项副标题展示 `N 个线程 · 最近:某线程` ### Android 会话首页与搜索 - Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/MainActivity.java` - Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityPinnedConversationsTest.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityRootListAdapterTest.java` 职责: - 首页按聚合后的会话项渲染 - 搜索命中多线程项目里的线程时,结果显示 `项目名 / 线程名` - 点击后仍进入文件夹页,不直接打平首页结构 ### Android 文件夹页与回跳 - Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/ConversationFolderActivityTest.java` 职责: - 把现有线程列表页收成“项目内部线程页” - 文件夹页标题、副标题、线程数和线程行展示与首页语义一致 - 支持从搜索结果进入时定位到对应线程 ### 文档与回归 - Modify: `/Users/kris/code/boss/README.md` - Modify: `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md` 职责: - 记录首页项目文件夹规则和搜索/导航行为 - 记录“单线程直显、2+ 线程归档”的正式产品逻辑 --- ### Task 1: 收紧服务端首页聚合规则 **Files:** - Modify: `/Users/kris/code/boss/src/lib/boss-projections.ts` - Test: `/Users/kris/code/boss/tests/conversation-home-items.test.ts` - [ ] **Step 1: 先写失败测试,锁住文件夹会话的副标题、时间、预览和上下文环来源** ```ts test("folder archive item uses latest thread preview and most urgent context thread", 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", "Android UI 收尾", "thread-1", "2026-04-05T10:00:00+08:00", ), buildImportedThreadProject( "mac-studio", "boss-thread-2", "Boss", "boss", "发布回滚", "thread-2", "2026-04-05T11:00:00+08:00", ), ); state.projects[0]!.preview = "旧预览"; state.projects[1]!.preview = "最近:发布回滚"; state.threadContextSnapshots = [ { snapshotId: "snapshot-a", workerId: "mac-studio", projectId: "boss-thread-1", threadId: "thread-1", title: "Android UI 收尾", summary: "", contextBudgetRemainingPct: 72, contextBudgetLevel: "safe", mustFinishBeforeCompaction: false, estimatedRemainingTurns: 20, estimatedRemainingLargeMessages: 10, compactionCount: 0, patchPending: false, testsPending: false, evidencePending: false, checklist: [], capturedAt: "2026-04-05T10:05:00+08:00", }, { snapshotId: "snapshot-b", workerId: "mac-studio", projectId: "boss-thread-2", threadId: "thread-2", title: "发布回滚", summary: "", contextBudgetRemainingPct: 18, contextBudgetLevel: "critical", mustFinishBeforeCompaction: true, estimatedRemainingTurns: 2, estimatedRemainingLargeMessages: 1, compactionCount: 1, patchPending: false, testsPending: false, evidencePending: false, checklist: [], capturedAt: "2026-04-05T11:02:00+08:00", }, ]; const items = getConversationHomeItems(state); const folder = items.find((item) => item.conversationType === "folder_archive"); assert.ok(folder); assert.equal(folder?.projectTitle, "发布回滚"); assert.equal(folder?.threadTitle, "Boss"); assert.equal(folder?.threadCount, 2); assert.equal(folder?.preview, "最近:发布回滚"); assert.match(folder?.folderLabel ?? "", /^2 个线程 · 最近:发布回滚$/); assert.equal(folder?.latestReplyAt, "2026-04-05T11:00:00+08:00"); assert.equal(folder?.contextBudgetIndicator.percent, 18); assert.equal(folder?.contextBudgetIndicator.level, "critical"); assert.equal(folder?.mustFinishBeforeCompaction, true); }); ``` - [ ] **Step 2: 跑测试,确认先失败** Run: ```bash npx --yes tsx --test /Users/kris/code/boss/tests/conversation-home-items.test.ts ``` Expected: - FAIL,提示 `folderLabel`、`preview`、`projectTitle` 或上下文环来源与预期不一致 - [ ] **Step 3: 在首页聚合里补全文件夹项来源规则** 在 `/Users/kris/code/boss/src/lib/boss-projections.ts` 的 `getConversationHomeItems()` 中,把当前文件夹项构造收成: ```ts const latestItem = [...items].sort((a, b) => b.latestReplyAt.localeCompare(a.latestReplyAt))[0]; const topContextItem = [...items] .filter((item) => item.contextBudgetIndicator.visible) .sort((a, b) => { if (a.mustFinishBeforeCompaction !== b.mustFinishBeforeCompaction) { return a.mustFinishBeforeCompaction ? -1 : 1; } const aLevel = a.contextBudgetIndicator.level ?? "safe"; const bLevel = b.contextBudgetIndicator.level ?? "safe"; if (levelPriority[aLevel] !== levelPriority[bLevel]) { return levelPriority[aLevel] - levelPriority[bLevel]; } return b.latestReplyAt.localeCompare(a.latestReplyAt); })[0] ?? latestItem; const recentThreadLabel = latestItem.threadTitle?.trim(); const folderSubtitle = recentThreadLabel ? `${items.length} 个线程 · 最近:${recentThreadLabel}` : `${items.length} 个线程`; passthrough.push({ conversationId: `folder-${folderKey}`, conversationType: "folder_archive", projectId: folderKey, projectTitle: latestItem.projectTitle, threadTitle: project?.threadMeta?.folderName?.trim() || project?.name || latestItem.folderLabel || "项目文件夹", folderLabel: folderSubtitle, folderKey, threadCount: items.length, preview: latestItem.preview, lastMessagePreview: latestItem.lastMessagePreview, activityIconCount: Math.max(...items.map((item) => item.activityIconCount)), topPinnedLabel: items.some((item) => item.topPinnedLabel) ? "置顶" : undefined, manualPinned: items.some((item) => item.manualPinned), latestReplyAt: latestItem.latestReplyAt, latestReplyLabel: latestItem.latestReplyLabel, unreadCount: items.reduce((sum, item) => sum + item.unreadCount, 0), riskLevel: topContextItem.riskLevel, activeDeviceCount: latestItem.activeDeviceCount, deviceNamesPreview: latestItem.deviceNamesPreview, avatar: latestItem.avatar, contextBudgetIndicator: topContextItem.contextBudgetIndicator, contextBudgetSourceNodeId: topContextItem.contextBudgetSourceNodeId, contextBudgetUpdatedAt: topContextItem.contextBudgetUpdatedAt, mustFinishBeforeCompaction: topContextItem.mustFinishBeforeCompaction, }); ``` - [ ] **Step 4: 再补两个边界测试,锁住 1 ↔ 2+ 升降级** 把下面两个测试补到 `/Users/kris/code/boss/tests/conversation-home-items.test.ts`: ```ts test("single thread project stays direct until a second thread appears", async () => { await setup(); const state = await readState(); state.projects = state.projects.filter((project) => project.id === "master-agent"); state.projects.push( buildImportedThreadProject( "mac-studio", "talking-thread-1", "Talking", "talking", "树莓派二代查询", "thread-1", "2026-04-05T12:00:00+08:00", ), ); let items = getConversationHomeItems(state); assert.equal(items.some((item) => item.projectId === "talking-thread-1"), true); assert.equal(items.some((item) => item.conversationType === "folder_archive"), false); state.projects.push( buildImportedThreadProject( "mac-studio", "talking-thread-2", "Talking", "talking", "语音桥接回归", "thread-2", "2026-04-05T12:05:00+08:00", ), ); items = getConversationHomeItems(state); assert.equal(items.some((item) => item.projectId === "talking-thread-1"), false); assert.equal(items.some((item) => item.projectId === "talking-thread-2"), false); assert.equal(items.some((item) => item.folderKey === "mac-studio:talking"), true); }); test("folder archive falls back to direct thread when project shrinks back to one thread", async () => { await setup(); const state = await readState(); state.projects = state.projects.filter((project) => project.id === "master-agent"); const first = buildImportedThreadProject("mac-studio", "talking-thread-1", "Talking", "talking", "树莓派二代查询", "thread-1", "2026-04-05T12:00:00+08:00"); const second = buildImportedThreadProject("mac-studio", "talking-thread-2", "Talking", "talking", "语音桥接回归", "thread-2", "2026-04-05T12:05:00+08:00"); state.projects.push(first, second); state.projects = state.projects.filter((project) => project.id !== "talking-thread-2"); const items = getConversationHomeItems(state); const direct = items.find((item) => item.projectId === "talking-thread-1"); assert.ok(direct); assert.equal(direct?.conversationType, "single_device"); assert.equal(items.some((item) => item.folderKey === "mac-studio:talking"), false); }); ``` - [ ] **Step 5: 重新跑投影测试,确认通过** Run: ```bash npx --yes tsx --test /Users/kris/code/boss/tests/conversation-home-items.test.ts ``` Expected: - PASS - [ ] **Step 6: 提交这一小步** ```bash git add /Users/kris/code/boss/src/lib/boss-projections.ts /Users/kris/code/boss/tests/conversation-home-items.test.ts git commit -m "feat: tighten conversation folder archive projections" ``` ### Task 2: 收平 Web 首页与搜索的文件夹心智 **Files:** - Modify: `/Users/kris/code/boss/src/components/app-ui.tsx` - Test: `/Users/kris/code/boss/tests/conversation-home-items.test.ts` - [ ] **Step 1: 先补失败测试,锁住 Web 文件夹副标题和搜索结果标签** 在 `/Users/kris/code/boss/tests/conversation-home-items.test.ts` 里新增一个只关心文案拼装的小测试: ```ts test("folder archive subtitle stays compact and search label preserves project and thread", 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", "Android UI 收尾", "thread-1", "2026-04-05T10:00:00+08:00"), buildImportedThreadProject("mac-studio", "boss-thread-2", "Boss", "boss", "发布回滚", "thread-2", "2026-04-05T11:00:00+08:00"), ); const [folder] = getConversationHomeItems(state).filter((item) => item.conversationType === "folder_archive"); assert.equal(folder?.folderLabel, "2 个线程 · 最近:发布回滚"); assert.equal(folder?.threadTitle, "Boss"); }); ``` - [ ] **Step 2: 跑测试,确认首页文案或搜索标签尚未完全匹配** Run: ```bash npx --yes tsx --test /Users/kris/code/boss/tests/conversation-home-items.test.ts ``` Expected: - FAIL,或当前文案与计划不一致 - [ ] **Step 3: 调整 Web 首页文件夹项文案和搜索命中展示** 在 `/Users/kris/code/boss/src/components/app-ui.tsx` 的会话列表渲染里,保持首页仍只吃 `ConversationItem[]`,但把文案收成: ```tsx const title = conversation.conversationType === "folder_archive" ? conversation.threadTitle : conversation.projectTitle; const subtitle = conversation.conversationType === "folder_archive" ? conversation.folderLabel : conversation.lastMessagePreview; ``` 并在搜索结果命中多线程项目线程时,展示: ```tsx const searchLabel = conversation.conversationType === "folder_archive" ? conversation.folderLabel : conversation.folderKey && conversation.folderLabel ? `${conversation.folderLabel} / ${conversation.threadTitle}` : conversation.threadTitle; ``` 同时把多线程项目的点击保持到文件夹页: ```tsx const href = conversation.conversationType === "folder_archive" && conversation.folderKey ? `/conversations/folders/${encodeURIComponent(conversation.folderKey)}` : `/conversations/${encodeURIComponent(conversation.projectId)}`; ``` - [ ] **Step 4: 重新跑投影测试,确认首页文案没回退** Run: ```bash npx --yes tsx --test /Users/kris/code/boss/tests/conversation-home-items.test.ts ``` Expected: - PASS - [ ] **Step 5: 提交这一小步** ```bash git add /Users/kris/code/boss/src/components/app-ui.tsx /Users/kris/code/boss/tests/conversation-home-items.test.ts git commit -m "feat: align web conversation folders with drawer design" ``` ### Task 3: 收平 Android 首页文件夹渲染与搜索 **Files:** - Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/MainActivity.java` - Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityPinnedConversationsTest.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityRootListAdapterTest.java` - [ ] **Step 1: 先写失败测试,锁住多线程项目在首页显示为文件夹而不是平铺线程** 在 `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityRootListAdapterTest.java` 增加: ```java @Test public void folderArchiveRowUsesProjectTitleAndCompactSubtitle() throws Exception { JSONObject folderItem = new JSONObject() .put("conversationType", "folder_archive") .put("projectId", "mac-studio:boss") .put("projectTitle", "Boss") .put("threadTitle", "Boss") .put("folderLabel", "2 个线程 · 最近:发布回滚") .put("threadCount", 2) .put("preview", "最近:发布回滚") .put("latestReplyLabel", "11:00") .put("activityIconCount", 0) .put("contextBudgetIndicator", new JSONObject() .put("visible", true) .put("style", "ring_percent") .put("percent", 18) .put("level", "critical")); WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(folderItem); assertEquals("Boss", row.threadTitle); assertEquals("2 个线程 · 最近:发布回滚", row.metaLine); assertEquals("最近:发布回滚", row.preview); } ``` - [ ] **Step 2: 写失败测试,锁住搜索命中多线程项目线程时保留项目归属** 在 `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java` 增加: ```java @Test public void searchResultsKeepFolderProjectContextForThreadsInsideArchives() throws Exception { TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class) .setup() .get(); activity.setConversationItems(new JSONArray() .put(new JSONObject() .put("conversationType", "folder_archive") .put("projectId", "mac-studio:boss") .put("projectTitle", "Boss") .put("threadTitle", "Boss") .put("folderKey", "mac-studio:boss") .put("folderLabel", "2 个线程 · 最近:发布回滚") .put("preview", "最近:发布回滚")) .put(new JSONObject() .put("conversationType", "single_device") .put("projectId", "boss-thread-2") .put("projectTitle", "发布回滚") .put("threadTitle", "发布回滚") .put("folderKey", "mac-studio:boss") .put("folderLabel", "Boss") .put("preview", "最近:发布回滚"))); activity.enterConversationSearchModeForTest(); activity.setConversationSearchQueryForTest("发布"); assertTrue(activity.latestRenderedSearchLabels().contains("Boss / 发布回滚")); } ``` - [ ] **Step 3: 跑 Android 定向测试,确认先失败** Run: ```bash cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.MainActivityConversationSearchTest --tests com.hyzq.boss.MainActivityRootListAdapterTest --tests com.hyzq.boss.MainActivityPinnedConversationsTest --no-daemon ``` Expected: - FAIL,提示行文案或搜索结果标签与新预期不一致 - [ ] **Step 4: 调整 Android 首页行文案和搜索标签** 在 `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java`,让 `folder_archive` 行走: ```java if ("folder_archive".equals(type)) { return new ConversationRow( item.optString("threadTitle", item.optString("projectTitle", "项目文件夹")), item.optString("folderLabel", ""), item.optString("preview", ""), item.optString("latestReplyLabel", "刚刚"), contextStatus, activityVisible, activityIconCount, avatarInitials ); } ``` 在 `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/MainActivity.java` 的搜索结果构建里,给多线程项目内线程加归属标签: ```java private String buildConversationSearchLabel(JSONObject item) { String conversationType = item.optString("conversationType", ""); if ("folder_archive".equals(conversationType)) { return item.optString("threadTitle", item.optString("projectTitle", "项目文件夹")); } String folderLabel = item.optString("folderLabel", "").trim(); String folderKey = item.optString("folderKey", "").trim(); String threadTitle = item.optString("threadTitle", item.optString("projectTitle", "会话")); if (!folderLabel.isEmpty() && !folderKey.isEmpty()) { return folderLabel + " / " + threadTitle; } return threadTitle; } ``` 并保证点击搜索结果时: ```java if (!folderKey.isEmpty() && !"folder_archive".equals(conversationType) && isArchivedProjectThread(item)) { openConversationFolder(folderKey, folderLabel, projectId); return; } ``` - [ ] **Step 5: 重新跑 Android 定向测试,确认通过** Run: ```bash cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.MainActivityConversationSearchTest --tests com.hyzq.boss.MainActivityRootListAdapterTest --tests com.hyzq.boss.MainActivityPinnedConversationsTest --no-daemon ``` Expected: - PASS - [ ] **Step 6: 提交这一小步** ```bash git add /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/MainActivity.java /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityConversationSearchTest.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityRootListAdapterTest.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/MainActivityPinnedConversationsTest.java git commit -m "feat: align android conversation folders with drawer design" ``` ### Task 4: 把 Android 文件夹页收成项目内部线程页 **Files:** - Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java` - Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/ConversationFolderActivityTest.java` - [ ] **Step 1: 写失败测试,锁住文件夹页标题、副标题和线程列表顺序** 在 `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/ConversationFolderActivityTest.java` 增加: ```java @Test public void folderScreenBehavesAsProjectThreadPage() throws Exception { Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestConversationFolderActivity.class) .putExtra(ConversationFolderActivity.EXTRA_FOLDER_KEY, "mac-studio:boss") .putExtra(ConversationFolderActivity.EXTRA_FOLDER_NAME, "Boss"); TestConversationFolderActivity activity = Robolectric .buildActivity(TestConversationFolderActivity.class, intent) .setup() .get(); activity.renderFolderForTest(new JSONObject() .put("folderLabel", "Boss") .put("deviceName", "Mac Studio") .put("threadCount", 2) .put("threads", new JSONArray() .put(new JSONObject() .put("projectId", "boss-thread-2") .put("conversationType", "single_device") .put("projectTitle", "发布回滚") .put("threadTitle", "发布回滚") .put("preview", "最近:发布回滚") .put("latestReplyLabel", "11:00")) .put(new JSONObject() .put("projectId", "boss-thread-1") .put("conversationType", "single_device") .put("projectTitle", "Android UI 收尾") .put("threadTitle", "Android UI 收尾") .put("preview", "最近:Android UI 收尾") .put("latestReplyLabel", "10:00")))); assertEquals("Boss", activity.topTitleText()); assertEquals("2 个线程", activity.topSubtitleText()); assertEquals(Arrays.asList("发布回滚", "Android UI 收尾"), activity.renderedThreadTitles()); } ``` - [ ] **Step 2: 跑文件夹页定向测试,确认先失败** Run: ```bash cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.ConversationFolderActivityTest --no-daemon ``` Expected: - FAIL,提示标题、副标题或线程列表语义与新预期不一致 - [ ] **Step 3: 把文件夹页标题和副标题改成项目内部线程页** 在 `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java` 调整: ```java String resolvedFolderName = folder.optString("folderLabel", folderName == null ? "项目线程" : folderName); int threadCount = folder.optInt("threadCount", 0); configureScreen(resolvedFolderName, threadCount + " 个线程"); appendContent(BossUi.buildSoftPanel( this, resolvedFolderName, threadCount + " 个线程", "选择线程后进入具体聊天窗口。" )); ``` 并在接收搜索跳转时支持可选的目标线程定位: ```java public static final String EXTRA_TARGET_PROJECT_ID = "target_project_id"; ``` 在渲染线程列表时,若命中目标线程: ```java if (projectId.equals(targetProjectId)) { row = row.withHighlighted(true); } ``` 如果当前 `ConversationRow` 没有高亮能力,本轮先用滚动到命中线程代替: ```java if (projectId.equals(targetProjectId)) { matchedThreadView = rowView; } ... if (matchedThreadView != null) { matchedThreadView.post(() -> matchedThreadView.requestFocus()); } ``` - [ ] **Step 4: 重新跑文件夹页测试,确认通过** Run: ```bash cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.ConversationFolderActivityTest --no-daemon ``` Expected: - PASS - [ ] **Step 5: 提交这一小步** ```bash git add /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/ConversationFolderActivityTest.java git commit -m "feat: refine android folder conversation screen" ``` ### Task 5: 文档、全量验证与发布前检查 **Files:** - Modify: `/Users/kris/code/boss/README.md` - Modify: `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md` - [ ] **Step 1: 更新 README,明确首页会话归档规则** 在 `/Users/kris/code/boss/README.md` 增加一段简洁说明: ```md ## 会话首页项目归档规则 - 单线程项目:首页直接显示线程会话 - 2 个及以上线程的项目:首页显示为一个项目文件夹会话 - 文件夹项时间取项目内最新活跃线程 - 文件夹项预览取项目内最新回复 - 文件夹项上下文环取项目内最需要关注的线程 - 搜索仍可命中线程,但多线程项目中的线程会先进入对应项目文件夹页 ``` - [ ] **Step 2: 更新运行状态文档,记录 Android/Web 均已收成项目文件夹心智** 在 `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md` 补一段: ```md ### 会话首页项目文件夹模式 - 单线程项目直接显示线程 - 多线程项目在首页聚合为项目文件夹会话 - Android 与 Web 均使用相同聚合规则 - 会话搜索保留线程可达性,但不再打破首页项目文件夹结构 ``` - [ ] **Step 3: 跑最终验证** Run: ```bash cd /Users/kris/code/boss && npx --yes tsx --test tests/conversation-home-items.test.ts cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.MainActivityConversationSearchTest --tests com.hyzq.boss.MainActivityPinnedConversationsTest --tests com.hyzq.boss.MainActivityRootListAdapterTest --tests com.hyzq.boss.ConversationFolderActivityTest --no-daemon cd /Users/kris/code/boss && npm run lint cd /Users/kris/code/boss && npm run build cd /Users/kris/code/boss/android && ./gradlew assembleRelease --no-daemon ``` Expected: - 全部 PASS - 没有新的 lint/build 回归 - [ ] **Step 4: 提交文档和验证收口** ```bash git add /Users/kris/code/boss/README.md /Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md git commit -m "docs: document conversation folder drawer behavior" ``` ## Self-Review - Spec coverage: - 首页单线程直显、多线程项目文件夹化:Task 1 / Task 3 - 文件夹项时间、预览、上下文环聚合:Task 1 - 搜索保留线程可达性但不打破文件夹心智:Task 2 / Task 3 - 文件夹页作为项目内部线程页:Task 4 - 置顶、排序、1 ↔ 2+ 升降级:Task 1 / Task 3 - 文档收口:Task 5 - Placeholder scan: - 已避免 `TODO/TBD/自行处理` - 每个任务都给了具体文件、测试和命令 - Type consistency: - 统一使用现有 `ConversationItem`、`folder_archive`、`folderKey`、`threadCount` - Android 继续沿用 `WechatSurfaceMapper.ConversationRow` 和 `ConversationFolderActivity`