AionUi 团队协作运行体验优化设计解析:身份色系统、视图切换与 Warmup 闸门实现
【免费下载链接】AionUiOpen-source 24/7 Cowork app for OpenClaw, Hermes, Claude Code, Codex, OpenCode and 20+ more CLI Agent | Customize your assistants | Team them up|Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/ai/AionUi
导读
本文基于 team-runtime-experience.design.md(及其配套 team-runtime-experience.md),系统讲解 AionUi 桌面端团队详情页的协作体验优化方案:如何用「身份色系统」解决多成员并行协作时分不清消息归属的问题,如何按团队记忆切换并行/单聊视图,以及如何用「Leader ready」闸门让团队初始化(Warmup)从静默卡顿变成有引导、不阻塞的流程。读完本文,你将理解整套方案的架构决策(全部限定渲染层、持久化走localStorage)、核心算法的纯函数实现,以及它在 packages/desktop/src/renderer/pages/team/ 下的真实落地代码,可直接对照源码复现与二次开发。
1. 背景与名词约定
在进入设计细节前,先明确几个贯穿全文的概念(定义见 PRD 名词节):
| 名词 | 含义 |
|---|---|
| 成员实例(slot) | 团队里的一个成员位,以slot_id唯一标识。同一个助手可以被拉进团队多次,每次是一个独立成员实例(不同slot_id),各自拥有独立会话 |
| Leader | 团队里role === 'leader'的成员,恒为成员列表第一个,不可移除、不可拖拽 |
| 身份色 | 团队详情页内为区分成员而临时赋予的颜色,仅在团队详情页生效,是纯展示辅助,不是业务数据 |
| Warmup | 进入团队后,后端拉起各成员运行时 + 注入协作环境(MCP)的初始化阶段 |
设计基调明确了两点:全部改动限定渲染层(前端),不改 aioncore 后端与团队数据结构;所有持久化一律走浏览器localStorage,不落库。配色取自 AionUi 既有 token(品牌色及其低饱和邻近色系),颜色只作「身份标识」,不做整格彩底,保持界面灰白干净的基调。
设计文档同时划定了本次范围:只优化「创建团队之后、在团队详情页协作」的体验,不涉及团队创建流程;头像容器统一、确认型弹窗轻量统一等被列为「后续单独专题」,不在本次范围内。
2. 现状调研:决定架构的关键事实
设计方案不是凭空而起,而是基于渲染层现有代码的九条事实(F1–F9)推导出来的。文档以表格形式给出,每条都附有出处,以下完整继承并给出源码层面的确认:
| # | 事实 | 出处(设计文档标注,路径均已换算为仓库根目录相对路径) | 对方案的影响 |
|---|---|---|---|
| F1 | TeamTabsProvider已是每团队一实例,已有按team_id分键的 localStorage(team-active-slot-*、team-assistant-order-*) | TeamTabsContext.tsx(team-active-slot-${team_id}出现在第 77 行,排序 key 前缀team-assistant-order-在第 35 行) | 身份色映射、视图模式复用同一层与同款 key 风格 |
| F2 | 运行时事件ITeamAgentRuntimeStatusEvent带slot_id+status: 'pending' / 'ready' / 'failed' | teamTypes.ts 中TeamAgentRuntimeStatus定义,事件体含slot_id、conversation_id、status、error? | 可单独判定 Leader ready→ Warmup 闸门成立 |
| F3 | membershipMutationBusy是全员聚合忙碌,不区分 Leader | teamMembershipMutationBusy.ts | Warmup 需在 session 层额外派生 leader-ready |
| F4 | teammate 消息只带senderConversationId(无slot_id) | 消息渲染链路 | 身份色按 conversation_id 取色;需要conversationId → 颜色的解析 |
| F5 | 发送框预填走getSendBoxDraftHook(type)(conversation_id).mutate(...),SWR 同 key 自动同步到 SendBox | 发送框草稿 hook 与TeamChatEmptyState建议卡 | 「告诉 Leader」直接复用,无需新机制 |
| F6 | 并行/全屏已有fullscreenSlotId(TeamPageContent 本地 state) | TeamPage.tsx | 升级为显式、持久化的viewMode(不新造全屏逻辑) |
| F7 | 空状态 Leader 分支有 subtitle + 3 建议卡;建议卡 onClick 为fillDraft(label) | TeamChatEmptyState.tsx | Leader 问候语替换 subtitle,纯文案改动 |
| F8 | 选中/列高亮当前散落在 TeamPage(leader 硬编码 primary 色)与 TeamTabs(active class) | TeamPage.tsx、TeamTabs.tsx | 身份色系统统一收口,替换这些硬编码 |
| F9 | 成员实例键为slot_id;conversation_id与slot_id在assistants[]上一一对应 | TeamPage.tsx、teamTypes | 建conversationId → slot_id索引即可打通消息与成员 |
架构含义(设计文档的核心判断):身份色的「真源」是slot_id(成员实例),但消息侧只认conversation_id。因此身份色系统对外暴露两个查询——colorOf(slot_id)与colorOfConversation(conversation_id),内部用assistants[]维护conversation_id → slot_id索引打通两者。
3. 模块总览
方案在现有TeamTabsProvider(每团队一实例)上叠加三个新 hook,并向 context 追加导出:
TeamTabsProvider (已存在,每团队一实例) ├── useTeamMemberColors(team_id, assistants) 新增:身份色真源 + 持久化 ├── useTeamViewMode(team_id) 新增:并行/单聊,持久化 ├── useTeamWarmup(team_id) 新增:warmup 闸门/进度 └── context 追加导出: colorOf / colorOfConversation / viewMode / setViewMode / warmup新增纯函数/组件:
team/identity/teamMemberColors.ts— 色板 + 分配算法(纯函数,可单测)team/identity/useTeamMemberColors.ts— 持久化 hook(含conversation_id → slot_id索引)team/identity/TeamIdentityContext.tsx— 身份色 contextteam/components/TeamMemberCapsuleBar— 胶囊成员栏(替换 TeamTabs 呈现)team/components/TeamWarmupOverlay.tsx— warmup 遮罩team/components/TeamViewToggle.tsx— 标题行视图切换- 改造:消息渲染(气泡色条 + 彩名)、
TeamChatEmptyState(问候语 + 告诉 Leader)、TeamPage(列高亮用身份色)
以上模块在 packages/desktop/src/renderer/pages/team/ 下均已落地:identity/(纯函数与 hook)、hooks/(useTeamViewMode.ts、useTeamWarmup.ts、TeamTabsContext.tsx等)、components/(TeamWarmupOverlay.tsx、TeamViewToggle.tsx、TeamTabs.tsx等)。
设计原则:身份色系统是唯一色源,所有用色处(胶囊/气泡/列/遮罩)都从它取,杜绝各处硬编码 primary(消除 F8 的分散)。
4. 身份色系统(PRD §1)
4.1 色板
设计文档给出了初始色板:低饱和 slate 邻近色,取自 AionUi 品牌基调;每色给 accent(主)+ soft(浅底)两档,浅底用color-mix在运行时计算,故源码只存 accent。
仓库落地实现见 teamMemberColors.ts:
/** 身份色板:品牌 slate 邻近的低饱和色。索引 0 固定给 Leader(品牌色)。 */ export const TEAM_MEMBER_PALETTE = [ 'var(--brand)', // 0 = Leader '#5c9ea4', // 雾青 '#b58a5e', // 暖褐 '#9481bf', // 藕紫 '#c07d97', // 豆沙玫 '#6ba07e', // 灰绿 '#4f8ac9', // 雾蓝 '#c99a4b', // 琥珀 ] as const; export const LEADER_COLOR_INDEX = 0;设计文档同时说明深色模式处理策略:这些 hex 在深色下仍是中性可辨的低饱和色;如需微调,后续在default-color-scheme.css暗色块加对应--team-mX覆盖,teamMemberColors改为引用var(--team-mX)。首版直接用 hex,避免铺开主题工作量。
4.2 分配算法:钉死 + 释放复用
身份色的稳定性规则(来自 PRD §1):颜色绑定成员实例(slot_id),一经分配即钉死;其他成员的新增/删除/重排都不改变一个成员已有的颜色;新成员取当前未被占用的颜色(优先复用被移除成员释放出的颜色);成员数超出色板时循环复用。
设计文档给出纯函数骨架,仓库实现见 assignMemberColors(实现略作整理,逻辑与设计一致):
export function assignMemberColors(prev: Record<string, number>, assistants: MemberLike[]): Record<string, number> { const next: Record<string, number> = {}; const used = new Set<number>(); const paletteLen = TEAM_MEMBER_PALETTE.length; // 1) Leader 固定色号 0 const leader = assistants.find((a) => a.role === 'leader'); if (leader) { next[leader.slot_id] = LEADER_COLOR_INDEX; used.add(LEADER_COLOR_INDEX); } // 2) 已分配过的成员沿用原色号(钉死) for (const a of assistants) { if (a.slot_id in next) continue; const previous = prev[a.slot_id]; if (previous !== undefined) { next[a.slot_id] = previous; used.add(previous); } } // 3) 新成员取未占用的最小非 0 色号;色板占满后对长度取模循环 let cursor = 1; const nextFreeIndex = (): number => { if (used.size < paletteLen - 1) { let idx = 1; while (used.has(idx)) idx++; return idx; } const idx = cursor % paletteLen || 1; // 保底非 0(0 属 Leader) cursor++; return idx; }; for (const a of assistants) { if (a.slot_id in next) continue; const idx = nextFreeIndex(); next[a.slot_id] = idx; used.add(idx); } return next; }算法要点:移除成员时该 slot 不在assistants[]里 → 自然从next消失 = 释放色号;其余 slot 走「沿用」分支而不变色。循环仅在成员数超过色板时发生(罕见),且循环项位置相隔远、不易混淆。
同文件还提供取色辅助函数 memberColorValue:未知 slot 回退到 Leader 色(安全兜底)。该纯函数有独立单测覆盖(分配/钉死/释放/循环用例),见 tests/unit/renderer/team/teamMemberColors.test.ts。
4.3 持久化与 Hook
设计文档给出的useTeamMemberColors骨架(key 为team-member-colors-${team_id},值 =Record<slot_id, colorIndex>)在仓库中完整落地为 useTeamMemberColors.ts:
export function useTeamMemberColors(team_id: string, assistants: TeamAssistant[]): TeamMemberColorResolver { const key = storageKey(team_id); // `team-member-colors-${team_id}` const [colorMap, setColorMap] = useState<Record<string, number>>(() => readColorMap(key)); // 成员列表变化时增量重算:新成员补色、被移除成员释放色,已有成员钉死不变。 useEffect(() => { setColorMap((prev) => { const next = assignMemberColors(prev, assistants); if (shallowEqual(prev, next)) return prev; try { localStorage.setItem(key, JSON.stringify(next)); } catch { // storage full / unavailable — 颜色仍在内存生效,忽略持久化失败 } return next; }); }, [assistants, key]); // conversation_id -> slot_id 索引:消息只携带 senderConversationId,需借此回到成员实例取色。 const conversationToSlot = useMemo(() => { const index: Record<string, string> = {}; for (const a of assistants) { if (a.conversation_id) index[a.conversation_id] = a.slot_id; } return index; }, [assistants]); return useMemo( () => ({ colorOf: (slot_id) => memberColorValue(colorMap, slot_id), colorOfConversation: (conversation_id) => memberColorValue(colorMap, conversationToSlot[conversation_id ?? '']), }), [colorMap, conversationToSlot] ); }实现细节:readColorMap对畸形 JSON 有 try/catch 兜底;shallowEqual避免无变化时无谓写 storage;持久化失败(storage 满/不可用)被吞掉,颜色仍在内存生效。conversationToSlot索引正是 F9 的落地——消息侧只认conversation_id,借助该索引回到成员实例取色。
挂载点:TeamTabsProvider已持有team_id+assistants,在 TeamTabsContext.tsx 中调用useTeamMemberColors,并将colorOf/colorOfConversation并入 context value(第 158-188 行),供 TeamTabs、TeamPage 列以及消息侧使用。
4.4 消息侧取色(F4 的解法)
MessageText目前不在 TeamTabsProvider 子树内的保证性不足(它在会话渲染链里)。设计文档给了两个方案:
- 方案 a(推荐):team 会话渲染链上已知
team_id与assistants,在AssistantChatSlot → TeamChatView传入一个resolveSenderColor(senderConversationId)回调,透传到 MessageList/MessageText。改动局部、不引入全局 context。 - 方案 b:新建一个
TeamIdentityContext提供colorOfConversation,MessageText 里useContext(可选、非 team 场景返回 undefined → 不显示色条)。
首版走 a:沿现有 props 链把resolveSenderColor传到消息组件;非团队消息该回调不存在 → 行为不变。仓库中 TeamIdentityContext.tsx 也已提供,供需要 context 的场景使用。
4.5 用色落地(CSS 变量注入)
统一做法:给需要着色的容器设style={{ '--mc': colorOf(slot_id) }},CSS 里用var(--mc)+color-mix出浅底:
- 胶囊底:
background: color-mix(in srgb, var(--mc) 9%, var(--bg-base));选中态 16% +box-shadow: 0 0 0 1.5px var(--mc) - 列选中:
box-shadow: inset 0 0 0 2px var(--mc);列头color-mix(... 8% ...) - 气泡:发送者名
color: var(--mc);气泡border-left: 3px solid var(--mc)
PRD 同时强调:身份色不作用于头像——头像保持原样(整图圆形,不加描边、不换底色),且颜色只作身份标识,不做整格彩底。
5. 视图切换:并行 / 单聊(PRD §4)
5.1 按团队记忆的 viewMode
useTeamViewMode(team_id):viewMode: 'parallel' | 'single',keyteam-view-mode-${team_id},默认'parallel'。挂TeamTabsProvider,context 暴露viewMode/setViewMode。
仓库实现见 useTeamViewMode.ts,并已扩展出第三种模式:
/** * - parallel:所有成员对话列并排(默认)。 * - single:全屏显示当前选中的成员。 * - board:只读的「消息 & 任务」看板视图。 */ export type TeamViewMode = 'parallel' | 'single' | 'board'; const storageKey = (team_id: string): string => `team-view-mode-${team_id}`; const readViewMode = (team_id: string): TeamViewMode => { try { const stored = localStorage.getItem(storageKey(team_id)); if (stored === 'single') return 'single'; if (stored === 'board' || stored === 'flow') return 'board'; // migrate legacy 'flow' return 'parallel'; } catch { return 'parallel'; } };注意readViewMode对旧值'flow'做了迁移(归入'board'),且读取失败一律回退'parallel',保证各团队独立记忆、默认并行。
5.2 TeamPage 渲染改造
设计文档给出的改造路径:把现有fullscreenSlotId ? 全屏 : 并行的判断,替换为viewMode === 'single' ? 单列(activeSlotId) : 并行:
- 单聊显示
activeSlotId对应成员(复用现全屏那段 JSX,slot 来源从fullscreenSlotId换成activeSlotId)。 fullscreenSlotId本地 state 移除;原「点全屏图标」改为「切到单聊 + switchTab 到该 slot」。- 选中成员被移除时回退 Leader:已有逻辑覆盖(TeamTabsContext.tsx 中检测
activeSlotId不在assistants[]时回退到 Leader 或第一个成员,并写回 storage)。 - 视图切换控件
TeamViewToggle放 ChatLayout 标题行右侧(实现见 TeamViewToggle.tsx)。 - Warmup 期间允许切视图:TeamViewToggle 不受 warmup 禁用影响(纯前端布局变化,不改团队状态)。
视图是团队整体的属性 → 按团队记忆;并行视图与单聊视图共用同一份选中状态(activeSlotId),两视图切换时保持连贯;单聊视图下当前成员被移除 → 回退到 Leader,仍留在单聊视图。
6. Warmup 初始化状态(PRD §7)
6.1 问题与闸门决策
进入团队时后端在拉起成员运行时、注入协作环境,现状是静默禁用若干操作,用户只觉得「卡了一下」,还可能去做无效操作(发消息、加成员)。方案的核心决策:
- 结束闸门 = Leader ready:遮罩在 Leader 运行时就绪时即撤除,不等全体成员——用户进团队第一件事是跟 Leader 说目标,Leader 一好就能开工;这也从根本上规避「一个成员起不来就卡死整个团队」。
- 遮罩期间禁止:添加成员、移除成员、重命名成员、发消息(遮罩盖住输入区,天然禁用)。
- 遮罩期间允许:切换视图、切换/浏览成员、滚动。
- 失败与超时兜底:某 teammate 失败 → 遮罩早已撤除,该成员在自己的胶囊/列上显示「启动失败 · 可重试」;Leader 失败或超时未就绪 → 遮罩转为错误态(重试/返回),防止后端未发事件等极端情况下无限等待。
技术前置确认:方案依赖「前端能单独判断 Leader 的运行时就绪信号」。该信号已从 F2 确认存在——ITeamAgentRuntimeStatusEvent带slot_id+status(见 teamTypes.ts,TeamAgentRuntimeStatus = 'dormant' | 'pending' | 'ready' | 'failed'),因此闸门方案成立且仍为纯前端。
6.2 useTeamWarmup:派生 leader-ready 与 session 状态机
设计文档给出初版骨架(监听agentRuntimeStatusChanged+ 超时定时器),仓库实现 useTeamWarmup.ts 在此基础上进一步演进为以 session 状态为主、runtime 状态为辅的双通道:
export type TeamWarmupPhase = 'warming' | 'ready' | 'error'; export function useTeamWarmup(team_id: string): TeamWarmupState { const [phase, setPhase] = useState<TeamWarmupPhase>(team_id ? 'warming' : 'ready'); const [runtimeStatus, setRuntimeStatus] = useState<Map<string, TeamWarmupMemberState>>(() => new Map()); const [ensureAttempt, setEnsureAttempt] = useState(0); useEffect(() => { if (!team_id) { setPhase('ready'); setRuntimeStatus(new Map()); return; } let cancelled = false; setPhase('warming'); setRuntimeStatus(new Map()); const unsubRuntime = ipcBridge.team.agentRuntimeStatusChanged.on((event: ITeamAgentRuntimeStatusEvent) => { if (event.team_id !== team_id || cancelled) return; setRuntimeStatus((prev) => { const next = new Map(prev); next.set(event.slot_id, { status: event.status, error: event.error }); return next; }); }); const unsubSessionStatus = ipcBridge.team.sessionStatusChanged.on((event: ITeamSessionStatusChangedEvent) => { if (event.team_id !== team_id || cancelled) return; if (event.status === 'starting') setPhase('warming'); else if (event.status === 'ready') setPhase('ready'); else if (event.status === 'failed') setPhase('error'); // 'stopped':idle 回收停止,交由发送框以可恢复提示呈现,不触发页面级遮罩 }); return () => { cancelled = true; unsubRuntime(); unsubSessionStatus(); }; }, [team_id]); useEffect(() => { if (!team_id) return; let cancelled = false; setPhase('warming'); ipcBridge.team.ensureSession .invoke({ team_id }) .then(() => { if (!cancelled) setPhase('ready'); }) .catch(() => { if (!cancelled) setPhase('error'); }); return () => { cancelled = true; }; }, [team_id, ensureAttempt]); const retry = useCallback(() => { if (!team_id) return; setPhase('warming'); setEnsureAttempt((attempt) => attempt + 1); }, [team_id]); return { phase, runtimeStatus, retry }; }设计文档中特别标注的「需实现时校验」问题(进入团队时 Leader 若已 ready,statusMap初值是否已是非 pending)在实现中被更稳健的方案取代:ensureSession.invoke的 Promise 作为事件遗漏时的兜底,同时 session 状态流(sessionStatusChanged)被视为 ready/failed 转换的权威来源,runtime 事件仅提供 per-slot 诊断细节(用于胶囊/列上的成员级失败态)。stopped状态不触发页面级遮罩,由发送框以可恢复提示呈现、下次发送时惰性恢复——这是从设计到实现的重要演进点。
6.3 遮罩组件与禁用清单
TeamWarmupOverlay(见 TeamWarmupOverlay.tsx)按 phase 分支渲染:
phase === 'warming':磨砂遮罩(backdrop-filter: blur(3px)+bg color-mix(--bg-1 78%))+ 成员头像从左到右逐个点亮(依据各 slot 的 statusMap ready 数)+「唤醒中 N/M」+ 品牌色进度条。phase === 'error':错误态卡片(文案 + 重试/返回)。重试 = 重新触发进入团队的初始化(复用现有进入路径 / 重建,不新增后端接口)。phase === 'ready':不渲染(撤除)。- 渲染位置:TeamPageContent 内容区之上(
.warmwrap定位父级,覆盖 chat 区,不盖标题行——标题行的视图切换仍可用)。
禁用清单接线:加/删/改成员已有membershipMutationBusy门控(TeamTabs、TeamPage 移除处理),warmup 与之高度重合,保持即可;加号(TeamAddMemberPopoverdisabled)同理;发消息被遮罩覆盖 chat 区(含 SendBox)天然禁用;允许切视图、切成员、滚动(控件在标题行/成员栏,不被遮罩覆盖)。
7. 成员栏胶囊化(PRD §2 / §3)
TeamMemberCapsuleBar替换TeamTabs的呈现,但保留其 context 消费与>switchTab(leaderSlotId); // 选中 Leader setViewMode('single'); // 切单聊全屏 Leader(可选,按体验) // 预填 Leader 会话草稿(F5),不自动发 const draft = getSendBoxDraftHook(kindOf(leaderConv.type), initial)(leaderConv.conversation_id); draft.mutate((prev) => ({ ...prev, content: t('team.addMember.tellLeaderPrefill') }));
核心思路来自 F5:发送框预填走getSendBoxDraftHook(type)(conversation_id).mutate(...),SWR 同 key 自动同步到 SendBox——「告诉 Leader」直接复用该机制,无需新机制。预填文案team.addMember.tellLeaderPrefill= 「帮我在团队里加一个擅长 ___ 的成员」,不自动发送,光标定位让用户补全后自行发出。
8.2 Leader 问候
TeamChatEmptyState的 Leader 空状态 subtitle 替换为(新 i18n keyteam.emptyState.leaderGreeting):
你好,我是 Leader,负责理解你的目标并协调团队。描述你想做的事,我来安排。
保留下方已有的 3 个提示词快捷卡片不动(F7:建议卡 onClick =fillDraft(label)纯文案层面替换 subtitle)。入口不做空状态——用户一定有可添加的助手。
9. i18n 新增 key
| key | 用途 |
|---|---|
team.view.parallel/team.view.single | 视图切换标签 |
team.warmup.title/team.warmup.progress(带 {n}/{m}) | 遮罩文案 |
team.warmup.timeout/team.warmup.leaderFailed/team.warmup.retry | 错误态 |
team.member.startFailed/team.member.retry | teammate 失败态 |
team.addMember.tellLeaderHint/team.addMember.tellLeaderCta/team.addMember.tellLeaderPrefill | 告诉 Leader |
team.emptyState.leaderGreeting | Leader 问候 |
准确翻译 en-US / zh-CN / zh-TW,其余 locale 英文兜底;改后跑node scripts/generate-i18n-types.js+node scripts/check-i18n.js(脚本位于 scripts/ 目录)。
10. 分阶段落地与验收
每阶段流程:改动 →bunx tsc --noEmit+oxlint+ 相关单测 +bun run package起 dev/CDP 自查 → 交验收 → 通过后按聚焦 commit。
| 阶段 | 内容 | 独立验收点 |
|---|---|---|
| 1 | 身份色系统 + 胶囊成员栏 + 气泡区分 + 选中态高亮 | 多成员下颜色区分清晰、选中态明显、增删成员颜色稳定(别人不变色) |
| 2 | 并行/单聊视图切换(按团队记忆) | 两视图切换连贯、按团队记忆、选中态共享、移除回退 Leader |
| 3 | Warmup 状态 + 禁用清单 + 失败/超时兜底 | Leader 就绪即可用、某成员失败不卡死、超时有兜底、期间可切视图、发消息被挡 |
| 4 | 添加成员虚线胶囊 + 「找 Leader 拉人」引导 + Leader 问候语 | 入口常驻可见、引导可切 Leader 预填不自动发、问候到位 |
11. 风险与回归
设计文档列出的四类风险值得在实现与回归时逐一核对:
- 测试契约:胶囊栏替换 TeamTabs DOM,需同步 team 相关单测/E2E 的 selector(本仓有 [E2E SYNC] 约定)。身份色纯函数的单元测试已先行落地(teamMemberColors.test.ts)。
- 主题回归:身份色用 hex + color-mix,深色模式需目视核对;多套自定义主题用
:has()覆盖过 modal,需确认不误伤团队页新类。 - leader-ready 信号:阶段 3 实现前必须先在代码/CDP 确认信号可取。该信号已由
ITeamAgentRuntimeStatusEvent的slot_id+status字段确认存在(teamTypes.ts),且实现侧额外引入sessionStatusChanged+ensureSession兜底,形成双通道冗余。 - 性能:身份色映射为 O(成员数) 纯函数,随
assistants变化重算,无忧。
结语
这套方案的价值在于:把「多成员分不清、初始化静默卡顿」两个体验问题,收敛为三个可独立测试的渲染层能力——slot_id为真源的身份色系统(colorOf/colorOfConversation双查询 + localStorage 持久化)、按团队记忆的视图模式、以 Leader ready 为闸门的 Warmup 状态机。所有状态归属与存储位置在 PRD 附录 A 中有完整清单(视图模式、身份色映射走新 key,当前选中成员、成员排序复用既有 key),全部为纯前端改动,不改 aioncore 与团队数据结构。对照 packages/desktop/src/renderer/pages/team/ 下的落地代码,即可完整复现这套团队协作体验的优化链路。
【免费下载链接】AionUiOpen-source 24/7 Cowork app for OpenClaw, Hermes, Claude Code, Codex, OpenCode and 20+ more CLI Agent | Customize your assistants | Team them up|Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/ai/AionUi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考