feat: tighten conversation folder archive projections

This commit is contained in:
kris
2026-04-05 13:14:15 +08:00
parent ef17947635
commit 20b296ce4f
2 changed files with 160 additions and 3 deletions

View File

@@ -517,13 +517,17 @@ export function getConversationHomeItems(state: BossState): ConversationItem[] {
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 (a.contextBudgetIndicator.percent ?? 100) - (b.contextBudgetIndicator.percent ?? 100);
return b.latestReplyAt.localeCompare(a.latestReplyAt);
})[0];
const recentThreadLabel = latestItem.threadTitle.trim();
passthrough.push({
conversationId: `folder-${folderKey}`,
conversationType: "folder_archive",
@@ -534,7 +538,7 @@ export function getConversationHomeItems(state: BossState): ConversationItem[] {
threadTitle:
project?.threadMeta.folderName ??
(latestItem.folderLabel || latestItem.threadTitle),
folderLabel: `${device?.name ?? latestItem.deviceNamesPreview[0] ?? "设备"} · ${items.length} 个线程`,
folderLabel: recentThreadLabel ? `${items.length} 个线程 · 最近:${recentThreadLabel}` : `${items.length} 个线程`,
folderKey,
threadCount: items.length,
preview:
@@ -565,7 +569,9 @@ export function getConversationHomeItems(state: BossState): ConversationItem[] {
percent: topContextItem?.contextBudgetIndicator.percent ?? 100,
level: topContextItem?.contextBudgetIndicator.level ?? "safe",
},
mustFinishBeforeCompaction: items.some((item) => item.mustFinishBeforeCompaction),
contextBudgetSourceNodeId: topContextItem?.contextBudgetSourceNodeId,
contextBudgetUpdatedAt: topContextItem?.contextBudgetUpdatedAt,
mustFinishBeforeCompaction: Boolean(topContextItem?.mustFinishBeforeCompaction),
});
}

View File

@@ -65,6 +65,157 @@ function buildImportedThreadProject(deviceId: string, id: string, folderName: st
};
}
function buildThreadContextSnapshot(
projectId: string,
threadId: string,
title: string,
contextBudgetLevel: "safe" | "watch" | "urgent" | "critical",
mustFinishBeforeCompaction: boolean,
contextBudgetRemainingPct: number,
capturedAt: string,
nodeId: string,
) {
return {
snapshotId: `${projectId}-${threadId}-snapshot`,
projectId,
taskId: `${projectId}-${threadId}-task`,
threadId,
title,
summary: `${title} 的线程状态`,
nodeId,
workerId: "worker-1",
sourceKind: "worker_estimator",
status: "context_urgent",
contextBudgetRemainingPct,
contextBudgetLevel,
mustFinishBeforeCompaction,
estimatedRemainingTurns: 4,
estimatedRemainingLargeMessages: 2,
compactionCount: 0,
patchPending: false,
testsPending: false,
evidencePending: false,
checklist: [],
capturedAt,
} satisfies import("../src/lib/boss-data").ThreadContextSnapshot;
}
test("folder archives use the latest thread preview/time while subtitle and context ring come from the right threads", 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-latest",
"Boss",
"boss",
"最新线程",
"thread-latest",
"2026-04-04T12:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-urgent",
"Boss",
"boss",
"优先收尾",
"thread-urgent",
"2026-04-04T11:00:00+08:00",
),
);
state.threadContextSnapshots = [
buildThreadContextSnapshot(
"boss-thread-latest",
"thread-latest",
"最新线程",
"critical",
false,
12,
"2026-04-04T12:05:00+08:00",
"node-latest",
),
buildThreadContextSnapshot(
"boss-thread-urgent",
"thread-urgent",
"优先收尾",
"critical",
true,
87,
"2026-04-04T11:05:00+08:00",
"node-urgent",
),
];
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
assert.equal(folder?.threadTitle, "Boss");
assert.equal(folder?.folderLabel, "2 个线程 · 最近:最新线程");
assert.equal(folder?.preview, "最近消息:最新线程");
assert.equal(folder?.lastMessagePreview, "最近消息:最新线程");
assert.equal(folder?.latestReplyAt, "2026-04-04T12:00:00+08:00");
assert.equal(folder?.latestReplyLabel, formatTimestampLabel("2026-04-04T12:00:00+08:00"));
assert.equal(folder?.contextBudgetIndicator.level, "critical");
assert.equal(folder?.contextBudgetIndicator.percent, 87);
assert.equal(folder?.mustFinishBeforeCompaction, true);
assert.equal(folder?.contextBudgetSourceNodeId, "node-urgent");
assert.equal(folder?.contextBudgetUpdatedAt, "2026-04-04T11:05:00+08:00");
});
test("conversation home upgrades and downgrades a folder archive as thread count crosses 1 and 2", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-04-04T10:00:00+08:00",
),
);
let items = getConversationHomeItems(state);
let direct = items.find((item) => item.projectId === "boss-thread-1");
assert.ok(direct, "expected a single thread to remain direct");
assert.equal(direct?.conversationType, "single_device");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-2",
"Boss",
"boss",
"发布回滚",
"thread-2",
"2026-04-04T11:00:00+08:00",
),
);
items = getConversationHomeItems(state);
const folder = items.find((item) => item.conversationType === "folder_archive" && item.folderKey === "mac-studio:boss");
assert.ok(folder, "expected folder archive once the folder has 2 threads");
assert.equal(folder?.threadCount, 2);
assert.equal(items.some((item) => item.projectId === "boss-thread-1"), false);
assert.equal(items.some((item) => item.projectId === "boss-thread-2"), false);
state.projects = state.projects.filter((project) => project.id !== "boss-thread-2");
items = getConversationHomeItems(state);
direct = items.find((item) => item.projectId === "boss-thread-1");
assert.ok(direct, "expected a single remaining thread to downgrade back to direct");
assert.equal(direct?.conversationType, "single_device");
});
test("conversation home groups multiple imported threads by folder while keeping single-thread projects direct", async () => {
await setup();
const state = await readState();