docs: add conversation folder drawer implementation plan
This commit is contained in:
745
docs/superpowers/plans/2026-04-05-conversation-folder-drawer.md
Normal file
745
docs/superpowers/plans/2026-04-05-conversation-folder-drawer.md
Normal file
@@ -0,0 +1,745 @@
|
||||
# 会话首页项目文件夹与抽屉模式 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`
|
||||
|
||||
Reference in New Issue
Block a user