feat: restore wechat thread ui and group chat
This commit is contained in:
@@ -130,6 +130,25 @@ export interface VersionEntry {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ThreadConversationMeta {
|
||||
projectId: string;
|
||||
threadId: string;
|
||||
threadDisplayName: string;
|
||||
folderName: string;
|
||||
activityIconCount: number;
|
||||
updatedAt: string;
|
||||
codexThreadRef?: string;
|
||||
codexFolderRef?: string;
|
||||
}
|
||||
|
||||
export interface GroupConversationMember {
|
||||
projectId: string;
|
||||
deviceId: string;
|
||||
threadId: string;
|
||||
threadDisplayName: string;
|
||||
folderName: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -140,6 +159,11 @@ export interface Project {
|
||||
updatedAt: string;
|
||||
lastMessageAt: string;
|
||||
isGroup: boolean;
|
||||
threadMeta: ThreadConversationMeta;
|
||||
groupMembers: GroupConversationMember[];
|
||||
createdByAgent: boolean;
|
||||
collaborationMode: "development" | "approval_required";
|
||||
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
|
||||
unreadCount: number;
|
||||
riskLevel: RiskLevel;
|
||||
contextBudgetPct?: number;
|
||||
@@ -698,6 +722,20 @@ const initialState: BossState = {
|
||||
updatedAt: "2026-03-25T12:06:00+08:00",
|
||||
lastMessageAt: "2026-03-25T12:06:00+08:00",
|
||||
isGroup: false,
|
||||
threadMeta: {
|
||||
projectId: "master-agent",
|
||||
threadId: "thread-master-main",
|
||||
threadDisplayName: "主 Agent 汇总",
|
||||
folderName: "主控线程",
|
||||
activityIconCount: 1,
|
||||
updatedAt: "2026-03-25T12:06:00+08:00",
|
||||
codexThreadRef: "thread-master-main",
|
||||
codexFolderRef: "master-agent",
|
||||
},
|
||||
groupMembers: [],
|
||||
createdByAgent: true,
|
||||
collaborationMode: "development",
|
||||
approvalState: "not_required",
|
||||
unreadCount: 0,
|
||||
riskLevel: "medium",
|
||||
contextBudgetPct: 71,
|
||||
@@ -724,6 +762,20 @@ const initialState: BossState = {
|
||||
updatedAt: "2026-03-25T11:52:00+08:00",
|
||||
lastMessageAt: "2026-03-25T11:52:00+08:00",
|
||||
isGroup: false,
|
||||
threadMeta: {
|
||||
projectId: "boss-console",
|
||||
threadId: "thread-boss-ui",
|
||||
threadDisplayName: "北区试产线回归",
|
||||
folderName: "归档确认",
|
||||
activityIconCount: 1,
|
||||
updatedAt: "2026-03-25T11:52:00+08:00",
|
||||
codexThreadRef: "thread-boss-ui",
|
||||
codexFolderRef: "boss-console",
|
||||
},
|
||||
groupMembers: [],
|
||||
createdByAgent: true,
|
||||
collaborationMode: "development",
|
||||
approvalState: "not_required",
|
||||
unreadCount: 2,
|
||||
riskLevel: "medium",
|
||||
contextBudgetPct: 62,
|
||||
@@ -795,6 +847,35 @@ const initialState: BossState = {
|
||||
updatedAt: "2026-03-25T10:58:00+08:00",
|
||||
lastMessageAt: "2026-03-25T10:58:00+08:00",
|
||||
isGroup: true,
|
||||
threadMeta: {
|
||||
projectId: "audit-collab",
|
||||
threadId: "thread-audit-chief",
|
||||
threadDisplayName: "审计对话",
|
||||
folderName: "审计群聊",
|
||||
activityIconCount: 2,
|
||||
updatedAt: "2026-03-25T10:58:00+08:00",
|
||||
codexThreadRef: "thread-audit-chief",
|
||||
codexFolderRef: "audit-collab",
|
||||
},
|
||||
groupMembers: [
|
||||
{
|
||||
projectId: "audit-collab",
|
||||
deviceId: "mac-studio",
|
||||
threadId: "thread-audit-chief",
|
||||
threadDisplayName: "审计对话",
|
||||
folderName: "审计群聊",
|
||||
},
|
||||
{
|
||||
projectId: "audit-collab",
|
||||
deviceId: "win-gpu-01",
|
||||
threadId: "thread-audit-hardware",
|
||||
threadDisplayName: "Windows 摄像头证据",
|
||||
folderName: "审计群聊",
|
||||
},
|
||||
],
|
||||
createdByAgent: true,
|
||||
collaborationMode: "development",
|
||||
approvalState: "not_required",
|
||||
unreadCount: 1,
|
||||
riskLevel: "high",
|
||||
messages: [
|
||||
@@ -1267,6 +1348,140 @@ function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function normalizeThreadMeta(
|
||||
raw: Partial<ThreadConversationMeta> | undefined,
|
||||
project: { id: string; name: string; isGroup: boolean; updatedAt: string },
|
||||
fallback?: ThreadConversationMeta,
|
||||
): ThreadConversationMeta {
|
||||
return {
|
||||
projectId: raw?.projectId ?? project.id,
|
||||
threadId: raw?.threadId ?? `thread-${project.id}`,
|
||||
threadDisplayName: raw?.threadDisplayName ?? project.name,
|
||||
folderName: raw?.folderName ?? fallback?.folderName ?? (project.isGroup ? "群聊" : project.name),
|
||||
activityIconCount: Math.max(1, raw?.activityIconCount ?? fallback?.activityIconCount ?? (project.isGroup ? 2 : 1)),
|
||||
updatedAt: raw?.updatedAt ?? project.updatedAt ?? nowIso(),
|
||||
codexThreadRef: raw?.codexThreadRef,
|
||||
codexFolderRef: raw?.codexFolderRef,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGroupMember(
|
||||
raw: Partial<GroupConversationMember>,
|
||||
fallbackProjectId: string,
|
||||
fallbackThreadMeta: ThreadConversationMeta,
|
||||
): GroupConversationMember {
|
||||
return {
|
||||
projectId: raw.projectId ?? fallbackProjectId,
|
||||
deviceId: raw.deviceId ?? "",
|
||||
threadId: raw.threadId ?? fallbackThreadMeta.threadId,
|
||||
threadDisplayName: raw.threadDisplayName ?? fallbackThreadMeta.threadDisplayName,
|
||||
folderName: raw.folderName ?? fallbackThreadMeta.folderName,
|
||||
};
|
||||
}
|
||||
|
||||
function dedupeStrings(values: string[]) {
|
||||
return [...new Set(values.filter((value) => Boolean(value)))];
|
||||
}
|
||||
|
||||
function dedupeGroupMembers(members: GroupConversationMember[]) {
|
||||
const seen = new Set<string>();
|
||||
const deduped: GroupConversationMember[] = [];
|
||||
for (const member of members) {
|
||||
const key = `${member.projectId}:${member.deviceId}:${member.threadId}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
deduped.push(member);
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function buildLegacyGroupMembers(
|
||||
projectId: string,
|
||||
deviceIds: string[],
|
||||
threadMeta: ThreadConversationMeta,
|
||||
) {
|
||||
return dedupeStrings(deviceIds).map((deviceId, index) => ({
|
||||
projectId,
|
||||
deviceId,
|
||||
threadId:
|
||||
index === 0 ? threadMeta.threadId : `${threadMeta.threadId}:${slugify(deviceId)}`,
|
||||
threadDisplayName: threadMeta.threadDisplayName,
|
||||
folderName: threadMeta.folderName,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeProjectConversationShape(
|
||||
project: Project,
|
||||
options?: {
|
||||
allowedDeviceIds?: Set<string>;
|
||||
},
|
||||
) {
|
||||
const allowedDeviceIds = options?.allowedDeviceIds;
|
||||
const normalizedThreadMeta = {
|
||||
...project.threadMeta,
|
||||
projectId: project.id,
|
||||
};
|
||||
const normalizedExplicitMembers = dedupeGroupMembers(
|
||||
project.groupMembers.map((member) =>
|
||||
normalizeGroupMember(member, project.id, normalizedThreadMeta),
|
||||
),
|
||||
);
|
||||
const hasExplicitGroupMembers = normalizedExplicitMembers.length > 0;
|
||||
const legacyGroupRequested = !hasExplicitGroupMembers && project.isGroup;
|
||||
const resolvedGroupMembers = hasExplicitGroupMembers
|
||||
? normalizedExplicitMembers
|
||||
: legacyGroupRequested
|
||||
? buildLegacyGroupMembers(project.id, project.deviceIds, normalizedThreadMeta)
|
||||
: [];
|
||||
const filteredGroupMembers = allowedDeviceIds
|
||||
? resolvedGroupMembers.filter((member) => allowedDeviceIds.has(member.deviceId))
|
||||
: resolvedGroupMembers;
|
||||
|
||||
if (filteredGroupMembers.length > 0) {
|
||||
project.isGroup = true;
|
||||
project.groupMembers = dedupeGroupMembers(filteredGroupMembers);
|
||||
project.deviceIds = dedupeStrings(project.groupMembers.map((member) => member.deviceId));
|
||||
project.threadMeta = {
|
||||
...normalizedThreadMeta,
|
||||
activityIconCount: Math.max(1, project.groupMembers.length),
|
||||
};
|
||||
return project;
|
||||
}
|
||||
|
||||
project.isGroup = false;
|
||||
project.groupMembers = [];
|
||||
project.deviceIds = allowedDeviceIds
|
||||
? project.deviceIds.filter((deviceId) => allowedDeviceIds.has(deviceId))
|
||||
: project.deviceIds;
|
||||
project.threadMeta = {
|
||||
...normalizedThreadMeta,
|
||||
activityIconCount: Math.max(1, normalizedThreadMeta.activityIconCount ?? 1),
|
||||
};
|
||||
return project;
|
||||
}
|
||||
|
||||
function resolveProjectUpdatedAt(project: Pick<Project, "updatedAt" | "lastMessageAt" | "threadMeta">, latestActivityAt?: string) {
|
||||
return latestIsoTimestamp(
|
||||
project.updatedAt,
|
||||
project.lastMessageAt,
|
||||
project.threadMeta.updatedAt,
|
||||
latestActivityAt,
|
||||
);
|
||||
}
|
||||
|
||||
function latestIsoTimestamp(...values: Array<string | undefined>) {
|
||||
let latestValue: string | undefined;
|
||||
let latestTime = 0;
|
||||
for (const value of values) {
|
||||
const valueTime = messageTimeValue(value);
|
||||
if (valueTime > latestTime) {
|
||||
latestTime = valueTime;
|
||||
latestValue = value;
|
||||
}
|
||||
}
|
||||
return latestValue ?? nowIso();
|
||||
}
|
||||
|
||||
function ensureArray<T>(value: T[] | undefined, fallback: T[]) {
|
||||
return Array.isArray(value) ? value : fallback;
|
||||
}
|
||||
@@ -1597,7 +1812,17 @@ function normalizeMessage(raw: Partial<Message>): Message {
|
||||
|
||||
function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
|
||||
const base = fallback ?? cloneInitialState().projects[0];
|
||||
return {
|
||||
const projectId = raw.id ?? base.id;
|
||||
const projectName = raw.name ?? base.name;
|
||||
const projectUpdatedAt = latestIsoTimestamp(raw.updatedAt, raw.lastMessageAt, base.updatedAt, base.lastMessageAt);
|
||||
const threadMetaFallback = fallback?.id === projectId ? fallback.threadMeta : undefined;
|
||||
const threadMeta = normalizeThreadMeta(raw.threadMeta, {
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
isGroup: raw.isGroup ?? base.isGroup ?? false,
|
||||
updatedAt: projectUpdatedAt,
|
||||
}, threadMetaFallback);
|
||||
const project: Project = {
|
||||
...base,
|
||||
...raw,
|
||||
pinned: raw.pinned ?? base.pinned,
|
||||
@@ -1620,7 +1845,17 @@ function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
|
||||
summary: version.summary ?? "",
|
||||
createdAt: version.createdAt ?? nowIso(),
|
||||
})),
|
||||
threadMeta,
|
||||
createdByAgent: raw.createdByAgent ?? false,
|
||||
collaborationMode: raw.collaborationMode ?? "development",
|
||||
approvalState: raw.approvalState ?? "not_required",
|
||||
};
|
||||
project.groupMembers = ensureArray(raw.groupMembers, []).map((member) =>
|
||||
normalizeGroupMember(member, projectId, project.threadMeta),
|
||||
);
|
||||
normalizeProjectConversationShape(project);
|
||||
project.updatedAt = resolveProjectUpdatedAt(project, project.threadMeta.updatedAt);
|
||||
return project;
|
||||
}
|
||||
|
||||
function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
@@ -2253,11 +2488,11 @@ function syncDerivedState(input: BossState) {
|
||||
|
||||
for (const project of state.projects) {
|
||||
project.deviceIds = project.deviceIds.filter((deviceId) => visibleDeviceIds.has(deviceId));
|
||||
project.isGroup = project.deviceIds.length > 1;
|
||||
const projectSnapshots = state.threadContextSnapshots
|
||||
.filter((snapshot) => snapshot.projectId === project.id)
|
||||
.sort(compareSnapshotsForRisk);
|
||||
|
||||
normalizeProjectConversationShape(project, { allowedDeviceIds: visibleDeviceIds });
|
||||
project.riskLevel = deriveRiskFromSnapshots(projectSnapshots);
|
||||
if (project.isGroup) {
|
||||
project.contextBudgetPct = undefined;
|
||||
@@ -2271,7 +2506,7 @@ function syncDerivedState(input: BossState) {
|
||||
}
|
||||
|
||||
project.lastMessageAt = latestProjectTimestamp(state, project.id);
|
||||
project.updatedAt = project.lastMessageAt;
|
||||
project.updatedAt = resolveProjectUpdatedAt(project, project.lastMessageAt);
|
||||
project.preview = deriveProjectPreview(state, project);
|
||||
project.unreadCount = Math.max(0, project.unreadCount ?? 0);
|
||||
}
|
||||
@@ -3826,6 +4061,150 @@ export async function updateConversationAction(
|
||||
return project;
|
||||
}
|
||||
|
||||
export async function renameProjectThread(input: {
|
||||
projectId: string;
|
||||
threadDisplayName: string;
|
||||
requestedBy: string;
|
||||
}) {
|
||||
const threadDisplayName = input.threadDisplayName.trim();
|
||||
if (!threadDisplayName) {
|
||||
throw new Error("THREAD_DISPLAY_NAME_REQUIRED");
|
||||
}
|
||||
|
||||
const project = await mutateState((state) => {
|
||||
const nextProject = state.projects.find((item) => item.id === input.projectId);
|
||||
if (!nextProject) throw new Error("PROJECT_NOT_FOUND");
|
||||
if (nextProject.isGroup) throw new Error("PROJECT_IS_GROUP_CHAT");
|
||||
|
||||
const updatedAt = nowIso();
|
||||
nextProject.name = threadDisplayName;
|
||||
nextProject.threadMeta.threadDisplayName = threadDisplayName;
|
||||
nextProject.threadMeta.updatedAt = updatedAt;
|
||||
nextProject.updatedAt = updatedAt;
|
||||
return nextProject;
|
||||
});
|
||||
publishBossEvent("conversation.updated", {
|
||||
projectId: input.projectId,
|
||||
note: `renamed by ${input.requestedBy}`,
|
||||
});
|
||||
return project;
|
||||
}
|
||||
|
||||
export async function renameGroupChat(input: {
|
||||
projectId: string;
|
||||
name: string;
|
||||
requestedBy: string;
|
||||
}) {
|
||||
const name = input.name.trim();
|
||||
if (!name) {
|
||||
throw new Error("GROUP_CHAT_NAME_REQUIRED");
|
||||
}
|
||||
|
||||
const project = await mutateState((state) => {
|
||||
const nextProject = state.projects.find((item) => item.id === input.projectId);
|
||||
if (!nextProject) throw new Error("PROJECT_NOT_FOUND");
|
||||
if (!nextProject.isGroup) throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||||
|
||||
const updatedAt = nowIso();
|
||||
nextProject.name = name;
|
||||
nextProject.threadMeta.threadDisplayName = name;
|
||||
nextProject.threadMeta.updatedAt = updatedAt;
|
||||
nextProject.updatedAt = updatedAt;
|
||||
return nextProject;
|
||||
});
|
||||
publishBossEvent("conversation.updated", {
|
||||
projectId: input.projectId,
|
||||
note: `renamed by ${input.requestedBy}`,
|
||||
});
|
||||
return project;
|
||||
}
|
||||
|
||||
export async function createProjectGroupChat(input: {
|
||||
sourceProjectId: string;
|
||||
memberProjectIds: string[];
|
||||
createdBy: string;
|
||||
}) {
|
||||
const project = await mutateState((state) => {
|
||||
const source = state.projects.find((item) => item.id === input.sourceProjectId);
|
||||
if (!source) throw new Error("GROUP_CHAT_SOURCE_NOT_FOUND");
|
||||
|
||||
const requestedProjectIds = [input.sourceProjectId, ...input.memberProjectIds];
|
||||
const memberProjects: Project[] = [];
|
||||
const seenProjectIds = new Set<string>();
|
||||
for (const projectId of requestedProjectIds) {
|
||||
if (seenProjectIds.has(projectId)) {
|
||||
continue;
|
||||
}
|
||||
seenProjectIds.add(projectId);
|
||||
|
||||
const memberProject = state.projects.find((item) => item.id === projectId);
|
||||
if (!memberProject) {
|
||||
throw new Error("GROUP_CHAT_MEMBER_NOT_FOUND");
|
||||
}
|
||||
memberProjects.push(memberProject);
|
||||
}
|
||||
if (memberProjects.length < 2) {
|
||||
throw new Error("GROUP_CHAT_REQUIRES_AT_LEAST_TWO_THREADS");
|
||||
}
|
||||
|
||||
const now = nowIso();
|
||||
const projectId = randomToken("project");
|
||||
const threadId = randomToken("thread");
|
||||
const threadDisplayName = source.threadMeta.threadDisplayName ?? source.name;
|
||||
const folderName = source.threadMeta.folderName ?? (source.isGroup ? "群聊" : source.name);
|
||||
const groupMembers = memberProjects.map((memberProject) => ({
|
||||
projectId: memberProject.id,
|
||||
deviceId: memberProject.deviceIds[0] ?? memberProject.id,
|
||||
threadId: memberProject.threadMeta.threadId,
|
||||
threadDisplayName: memberProject.threadMeta.threadDisplayName,
|
||||
folderName: memberProject.threadMeta.folderName,
|
||||
}));
|
||||
const nextProject = normalizeProject({
|
||||
id: projectId,
|
||||
name: threadDisplayName,
|
||||
pinned: false,
|
||||
systemPinned: false,
|
||||
deviceIds: dedupeStrings(groupMembers.map((member) => member.deviceId)),
|
||||
preview: `已创建群聊《${threadDisplayName}》`,
|
||||
updatedAt: now,
|
||||
lastMessageAt: now,
|
||||
isGroup: true,
|
||||
unreadCount: 0,
|
||||
riskLevel: source.riskLevel,
|
||||
threadMeta: {
|
||||
projectId,
|
||||
threadId,
|
||||
threadDisplayName,
|
||||
folderName,
|
||||
activityIconCount: Math.max(1, memberProjects.length),
|
||||
updatedAt: now,
|
||||
},
|
||||
groupMembers,
|
||||
createdByAgent: true,
|
||||
collaborationMode: "development",
|
||||
approvalState: "not_required",
|
||||
messages: [
|
||||
{
|
||||
id: randomToken("msg"),
|
||||
sender: "master",
|
||||
senderLabel: input.createdBy || "群聊创建",
|
||||
body: `已由 ${input.createdBy || "系统"} 创建群聊《${threadDisplayName}》。`,
|
||||
sentAt: now,
|
||||
kind: "text",
|
||||
},
|
||||
],
|
||||
goals: [],
|
||||
versions: [],
|
||||
});
|
||||
|
||||
state.projects.unshift(nextProject);
|
||||
return nextProject;
|
||||
});
|
||||
publishBossEvent("project.messages.updated", { projectId: project.id });
|
||||
publishBossEvent("conversation.updated", { projectId: project.id });
|
||||
return project;
|
||||
}
|
||||
|
||||
export async function appendProjectMessage(payload: {
|
||||
projectId: string;
|
||||
sender?: MessageSender;
|
||||
|
||||
@@ -34,7 +34,12 @@ export interface ConversationItem {
|
||||
conversationType: "master_agent" | "single_device" | "group";
|
||||
projectId: string;
|
||||
projectTitle: string;
|
||||
threadTitle: string;
|
||||
folderLabel: string;
|
||||
preview: string;
|
||||
lastMessagePreview: string;
|
||||
activityIconCount: number;
|
||||
topPinnedLabel?: "置顶";
|
||||
manualPinned: boolean;
|
||||
latestReplyAt: string;
|
||||
latestReplyLabel: string;
|
||||
@@ -47,6 +52,11 @@ export interface ConversationItem {
|
||||
secondary?: string;
|
||||
overflowCount?: number;
|
||||
};
|
||||
groupMembers?: Array<{
|
||||
threadId: string;
|
||||
avatar: string;
|
||||
title: string;
|
||||
}>;
|
||||
contextBudgetIndicator: ContextIndicator;
|
||||
contextBudgetSourceNodeId?: string;
|
||||
contextBudgetUpdatedAt?: string;
|
||||
@@ -170,6 +180,22 @@ function projectType(project: Project): ConversationItem["conversationType"] {
|
||||
return project.isGroup ? "group" : "single_device";
|
||||
}
|
||||
|
||||
function isTopPinnedConversation(project: Project) {
|
||||
return Boolean(project.pinned || project.systemPinned || project.id === "audit-collab");
|
||||
}
|
||||
|
||||
function getThreadAvatarFallback(title: string) {
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) return "A";
|
||||
return trimmed.slice(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
function getGroupMemberAvatar(member: Project["groupMembers"][number], device?: Device) {
|
||||
const avatar = device?.avatar?.trim();
|
||||
if (avatar) return avatar;
|
||||
return getThreadAvatarFallback(member.threadDisplayName);
|
||||
}
|
||||
|
||||
function aiRoleLabel(role: AiAccountRole) {
|
||||
switch (role) {
|
||||
case "primary":
|
||||
@@ -281,13 +307,32 @@ export function getConversationItems(state: BossState): 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),
|
||||
@@ -300,6 +345,7 @@ export function getConversationItems(state: BossState): ConversationItem[] {
|
||||
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",
|
||||
@@ -315,7 +361,9 @@ export function getConversationItems(state: BossState): ConversationItem[] {
|
||||
return conversations.sort((a, b) => {
|
||||
if (a.projectId === "master-agent") return -1;
|
||||
if (b.projectId === "master-agent") return 1;
|
||||
if (a.manualPinned !== b.manualPinned) return a.manualPinned ? -1 : 1;
|
||||
const aPinned = Boolean(a.topPinnedLabel);
|
||||
const bPinned = Boolean(b.topPinnedLabel);
|
||||
if (aPinned !== bPinned) return aPinned ? -1 : 1;
|
||||
return b.latestReplyAt.localeCompare(a.latestReplyAt);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user