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

28 KiB
Raw Permalink Blame History

会话首页项目文件夹与抽屉模式 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: 先写失败测试,锁住文件夹会话的副标题、时间、预览和上下文环来源

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:

npx --yes tsx --test /Users/kris/code/boss/tests/conversation-home-items.test.ts

Expected:

  • FAIL提示 folderLabelpreviewprojectTitle 或上下文环来源与预期不一致

  • Step 3: 在首页聚合里补全文件夹项来源规则

/Users/kris/code/boss/src/lib/boss-projections.tsgetConversationHomeItems() 中,把当前文件夹项构造收成:

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

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:

npx --yes tsx --test /Users/kris/code/boss/tests/conversation-home-items.test.ts

Expected:

  • PASS

  • Step 6: 提交这一小步

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 里新增一个只关心文案拼装的小测试:

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:

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[],但把文案收成:

const title =
  conversation.conversationType === "folder_archive"
    ? conversation.threadTitle
    : conversation.projectTitle;

const subtitle =
  conversation.conversationType === "folder_archive"
    ? conversation.folderLabel
    : conversation.lastMessagePreview;

并在搜索结果命中多线程项目线程时,展示:

const searchLabel =
  conversation.conversationType === "folder_archive"
    ? conversation.folderLabel
    : conversation.folderKey && conversation.folderLabel
      ? `${conversation.folderLabel} / ${conversation.threadTitle}`
      : conversation.threadTitle;

同时把多线程项目的点击保持到文件夹页:

const href =
  conversation.conversationType === "folder_archive" && conversation.folderKey
    ? `/conversations/folders/${encodeURIComponent(conversation.folderKey)}`
    : `/conversations/${encodeURIComponent(conversation.projectId)}`;
  • Step 4: 重新跑投影测试,确认首页文案没回退

Run:

npx --yes tsx --test /Users/kris/code/boss/tests/conversation-home-items.test.ts

Expected:

  • PASS

  • Step 5: 提交这一小步

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 增加:

@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 增加:

@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:

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 行走:

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 的搜索结果构建里,给多线程项目内线程加归属标签:

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;
}

并保证点击搜索结果时:

if (!folderKey.isEmpty() && !"folder_archive".equals(conversationType) && isArchivedProjectThread(item)) {
    openConversationFolder(folderKey, folderLabel, projectId);
    return;
}
  • Step 5: 重新跑 Android 定向测试,确认通过

Run:

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: 提交这一小步

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 增加:

@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:

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 调整:

String resolvedFolderName = folder.optString("folderLabel", folderName == null ? "项目线程" : folderName);
int threadCount = folder.optInt("threadCount", 0);
configureScreen(resolvedFolderName, threadCount + " 个线程");

appendContent(BossUi.buildSoftPanel(
        this,
        resolvedFolderName,
        threadCount + " 个线程",
        "选择线程后进入具体聊天窗口。"
));

并在接收搜索跳转时支持可选的目标线程定位:

public static final String EXTRA_TARGET_PROJECT_ID = "target_project_id";

在渲染线程列表时,若命中目标线程:

if (projectId.equals(targetProjectId)) {
    row = row.withHighlighted(true);
}

如果当前 ConversationRow 没有高亮能力,本轮先用滚动到命中线程代替:

if (projectId.equals(targetProjectId)) {
    matchedThreadView = rowView;
}
...
if (matchedThreadView != null) {
    matchedThreadView.post(() -> matchedThreadView.requestFocus());
}
  • Step 4: 重新跑文件夹页测试,确认通过

Run:

cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.ConversationFolderActivityTest --no-daemon

Expected:

  • PASS

  • Step 5: 提交这一小步

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 增加一段简洁说明:

## 会话首页项目归档规则

- 单线程项目:首页直接显示线程会话
- 2 个及以上线程的项目:首页显示为一个项目文件夹会话
- 文件夹项时间取项目内最新活跃线程
- 文件夹项预览取项目内最新回复
- 文件夹项上下文环取项目内最需要关注的线程
- 搜索仍可命中线程,但多线程项目中的线程会先进入对应项目文件夹页
  • Step 2: 更新运行状态文档,记录 Android/Web 均已收成项目文件夹心智

/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md 补一段:

### 会话首页项目文件夹模式

- 单线程项目直接显示线程
- 多线程项目在首页聚合为项目文件夹会话
- Android 与 Web 均使用相同聚合规则
- 会话搜索保留线程可达性,但不再打破首页项目文件夹结构
  • Step 3: 跑最终验证

Run:

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: 提交文档和验证收口

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:
    • 统一使用现有 ConversationItemfolder_archivefolderKeythreadCount
    • Android 继续沿用 WechatSurfaceMapper.ConversationRowConversationFolderActivity