Files
boss/docs/superpowers/plans/2026-04-05-conversation-folder-drawer.md

746 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 会话首页项目文件夹与抽屉模式 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`