+
+
+
导入 Codex 项目
+
+ {deviceName ?? deviceId} 完成首次 heartbeat 后,这里会出现可导入项目和线程。
+
+
+
+
+
+ {draft ? (
+
+ 候选线程:{draft.candidates.length}
+
+ 当前状态:{draft.status}
+
+ 已勾选:{selectedCandidateIds.length}
+
+ ) : (
+
+ 当前还没有导入草稿。请先让设备端完成配对并保持在线,然后回到这里点击刷新。
+
+ )}
+
+ {groups.map((group) => (
+
+
{group.folderName}
+
{group.items.length} 个线程
+
+ {group.items.map((candidate) => {
+ const selected = selectedCandidateIds.includes(candidate.candidateId);
+ return (
+
+ );
+ })}
+
+
+ ))}
+
+ {resolution ? (
+
+
{resolution.summary}
+
+ {resolution.items.map((item) => (
+
+ {item.threadDisplayName} · {item.folderName} · {item.action}
+
+ ))}
+
+
+ ) : null}
+
+
+
+
+
+
+ {message ? (
+
+ {message}
+
+ ) : null}
+
+ );
+}
diff --git a/src/lib/boss-projections.ts b/src/lib/boss-projections.ts
index b141743..bbe1683 100644
--- a/src/lib/boss-projections.ts
+++ b/src/lib/boss-projections.ts
@@ -33,11 +33,13 @@ export interface ContextIndicator {
export interface ConversationItem {
conversationId: string;
- conversationType: "master_agent" | "single_device" | "group";
+ conversationType: "master_agent" | "single_device" | "group" | "folder_archive";
projectId: string;
projectTitle: string;
threadTitle: string;
folderLabel: string;
+ folderKey?: string;
+ threadCount?: number;
preview: string;
lastMessagePreview: string;
activityIconCount: number;
@@ -184,6 +186,14 @@ function projectType(project: Project): ConversationItem["conversationType"] {
return project.isGroup ? "group" : "single_device";
}
+function buildFolderKey(project: Project) {
+ if (project.id === "master-agent" || project.isGroup) return undefined;
+ const deviceId = project.deviceIds[0];
+ const folderRef = project.threadMeta.codexFolderRef?.trim() || project.threadMeta.folderName.trim();
+ if (!deviceId || !folderRef) return undefined;
+ return `${deviceId}:${folderRef}`;
+}
+
function isTopPinnedConversation(project: Project) {
return Boolean(project.pinned || project.systemPinned || project.id === "audit-collab");
}
@@ -306,63 +316,64 @@ function threadViewsForProject(state: BossState, projectId: string) {
}));
}
-export function getConversationItems(state: BossState): ConversationItem[] {
- const conversations = state.projects.map((project) => {
- const devices = state.devices.filter((device) => project.deviceIds.includes(device.id));
- const threadViews = threadViewsForProject(state, project.id);
- const topThread = threadViews[0]?.snapshot;
- const threadTitle = project.threadMeta?.threadDisplayName ?? project.name;
- const folderLabel = project.threadMeta?.folderName ?? "";
- const activityIconCount = project.threadMeta?.activityIconCount ?? 1;
- const topPinnedLabel = isTopPinnedConversation(project) ? "置顶" : undefined;
- const groupMembers = project.isGroup
- ? project.groupMembers.map((member) => ({
- threadId: member.threadId,
- avatar: getGroupMemberAvatar(
- member,
- state.devices.find((device) => device.id === member.deviceId),
- ),
- title: member.threadDisplayName,
- }))
- : undefined;
+function buildConversationItem(state: BossState, project: Project): ConversationItem {
+ const devices = state.devices.filter((device) => project.deviceIds.includes(device.id));
+ const threadViews = threadViewsForProject(state, project.id);
+ const topThread = threadViews[0]?.snapshot;
+ const threadTitle = project.threadMeta?.threadDisplayName ?? project.name;
+ const folderLabel = project.threadMeta?.folderName ?? "";
+ const activityIconCount = project.threadMeta?.activityIconCount ?? 1;
+ const topPinnedLabel = isTopPinnedConversation(project) ? "置顶" : undefined;
+ const groupMembers = project.isGroup
+ ? project.groupMembers.map((member) => ({
+ threadId: member.threadId,
+ avatar: getGroupMemberAvatar(
+ member,
+ state.devices.find((device) => device.id === member.deviceId),
+ ),
+ title: member.threadDisplayName,
+ }))
+ : undefined;
- return {
- conversationId: `conv-${project.id}`,
- conversationType: projectType(project),
- projectId: project.id,
- projectTitle: project.name,
- threadTitle,
- folderLabel,
- preview: project.preview,
- lastMessagePreview: project.preview,
- activityIconCount,
- topPinnedLabel,
- manualPinned: Boolean(project.pinned && !project.systemPinned),
- latestReplyAt: project.lastMessageAt,
- latestReplyLabel: formatTimestampLabel(project.lastMessageAt),
- unreadCount: project.unreadCount,
- riskLevel: project.riskLevel,
- activeDeviceCount: devices.length,
- deviceNamesPreview: devices.map((device) => device.name),
- avatar: {
- primary: devices[0]?.avatar ?? "A",
- secondary: project.isGroup ? devices[1]?.avatar : undefined,
- overflowCount: Math.max(0, devices.length - 2) || undefined,
- },
- groupMembers,
- contextBudgetIndicator: {
- visible: !project.isGroup && Boolean(topThread),
- style: "ring_percent",
- percent: !project.isGroup ? topThread?.contextBudgetRemainingPct : undefined,
- level: !project.isGroup ? topThread?.contextBudgetLevel : undefined,
- },
- contextBudgetSourceNodeId: !project.isGroup ? topThread?.nodeId : undefined,
- contextBudgetUpdatedAt: !project.isGroup ? topThread?.capturedAt : undefined,
- mustFinishBeforeCompaction: Boolean(topThread?.mustFinishBeforeCompaction),
- } satisfies ConversationItem;
- });
+ return {
+ conversationId: `conv-${project.id}`,
+ conversationType: projectType(project),
+ projectId: project.id,
+ projectTitle: project.name,
+ threadTitle,
+ folderLabel,
+ folderKey: buildFolderKey(project),
+ preview: project.preview,
+ lastMessagePreview: project.preview,
+ activityIconCount,
+ topPinnedLabel,
+ manualPinned: Boolean(project.pinned && !project.systemPinned),
+ latestReplyAt: project.lastMessageAt,
+ latestReplyLabel: formatTimestampLabel(project.lastMessageAt),
+ unreadCount: project.unreadCount,
+ riskLevel: project.riskLevel,
+ activeDeviceCount: devices.length,
+ deviceNamesPreview: devices.map((device) => device.name),
+ avatar: {
+ primary: devices[0]?.avatar ?? "A",
+ secondary: project.isGroup ? devices[1]?.avatar : undefined,
+ overflowCount: Math.max(0, devices.length - 2) || undefined,
+ },
+ groupMembers,
+ contextBudgetIndicator: {
+ visible: !project.isGroup && Boolean(topThread),
+ style: "ring_percent",
+ percent: !project.isGroup ? topThread?.contextBudgetRemainingPct : undefined,
+ level: !project.isGroup ? topThread?.contextBudgetLevel : undefined,
+ },
+ contextBudgetSourceNodeId: !project.isGroup ? topThread?.nodeId : undefined,
+ contextBudgetUpdatedAt: !project.isGroup ? topThread?.capturedAt : undefined,
+ mustFinishBeforeCompaction: Boolean(topThread?.mustFinishBeforeCompaction),
+ } satisfies ConversationItem;
+}
- return conversations.sort((a, b) => {
+function sortConversationItems(items: ConversationItem[]) {
+ return items.sort((a, b) => {
if (a.projectId === "master-agent") return -1;
if (b.projectId === "master-agent") return 1;
const aPinned = Boolean(a.topPinnedLabel);
@@ -372,6 +383,128 @@ export function getConversationItems(state: BossState): ConversationItem[] {
});
}
+export function getConversationItems(state: BossState): ConversationItem[] {
+ const conversations = state.projects.map((project) => buildConversationItem(state, project));
+
+ return sortConversationItems(conversations);
+}
+
+export interface ConversationFolderView {
+ folderKey: string;
+ folderLabel: string;
+ deviceId?: string;
+ deviceName?: string;
+ threadCount: number;
+ threads: ConversationItem[];
+}
+
+export function getConversationHomeItems(state: BossState): ConversationItem[] {
+ const flatItems = getConversationItems(state);
+ const projectMap = new Map(state.projects.map((project) => [project.id, project]));
+ const grouped = new Map