AionUi 团队协作运行体验优化设计解析:身份色系统、视图切换与 Warmup 闸门实现
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 upStar 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推导出来的。文档以表格形式给出每条都附有出处以下完整继承并给出源码层面的确认#事实出处设计文档标注路径均已换算为仓库根目录相对路径对方案的影响F1TeamTabsProvider已是每团队一实例已有按team_id分键的 localStorageteam-active-slot-*、team-assistant-order-*TeamTabsContext.tsxteam-active-slot-${team_id}出现在第 77 行排序 key 前缀team-assistant-order-在第 35 行身份色映射、视图模式复用同一层与同款 key 风格F2运行时事件ITeamAgentRuntimeStatusEvent带slot_idstatus: pending / ready / failedteamTypes.ts 中TeamAgentRuntimeStatus定义事件体含slot_id、conversation_id、status、error?可单独判定 Leader ready→ Warmup 闸门成立F3membershipMutationBusy是全员聚合忙碌不区分 LeaderteamMembershipMutationBusy.tsWarmup 需在 session 层额外派生 leader-readyF4teammate 消息只带senderConversationId无slot_id消息渲染链路身份色按 conversation_id 取色需要conversationId → 颜色的解析F5发送框预填走getSendBoxDraftHook(type)(conversation_id).mutate(...)SWR 同 key 自动同步到 SendBox发送框草稿 hook 与TeamChatEmptyState建议卡「告诉 Leader」直接复用无需新机制F6并行/全屏已有fullscreenSlotIdTeamPageContent 本地 stateTeamPage.tsx升级为显式、持久化的viewMode不新造全屏逻辑F7空状态 Leader 分支有 subtitle 3 建议卡建议卡 onClick 为fillDraft(label)TeamChatEmptyState.tsxLeader 问候语替换 subtitle纯文案改动F8选中/列高亮当前散落在 TeamPageleader 硬编码 primary 色与 TeamTabsactive classTeamPage.tsx、TeamTabs.tsx身份色系统统一收口替换这些硬编码F9成员实例键为slot_idconversation_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 §14.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: Recordstring, number, assistants: MemberLike[]): Recordstring, number { const next: Recordstring, number {}; const used new Setnumber(); 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; // 保底非 00 属 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}值 Recordslot_id, colorIndex在仓库中完整落地为 useTeamMemberColors.tsexport function useTeamMemberColors(team_id: string, assistants: TeamAssistant[]): TeamMemberColorResolver { const key storageKey(team_id); // team-member-colors-${team_id} const [colorMap, setColorMap] useStateRecordstring, 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: Recordstring, 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_idassistants在 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提供colorOfConversationMessageText 里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 §45.1 按团队记忆的 viewModeuseTeamViewMode(team_id)viewMode: parallel | singlekeyteam-view-mode-${team_id}默认parallel。挂TeamTabsProvidercontext 暴露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对应成员复用现全屏那段 JSXslot 来源从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 §76.1 问题与闸门决策进入团队时后端在拉起成员运行时、注入协作环境现状是静默禁用若干操作用户只觉得「卡了一下」还可能去做无效操作发消息、加成员。方案的核心决策结束闸门 Leader ready遮罩在 Leader 运行时就绪时即撤除不等全体成员——用户进团队第一件事是跟 Leader 说目标Leader 一好就能开工这也从根本上规避「一个成员起不来就卡死整个团队」。遮罩期间禁止添加成员、移除成员、重命名成员、发消息遮罩盖住输入区天然禁用。遮罩期间允许切换视图、切换/浏览成员、滚动。失败与超时兜底某 teammate 失败 → 遮罩早已撤除该成员在自己的胶囊/列上显示「启动失败 · 可重试」Leader 失败或超时未就绪 → 遮罩转为错误态重试/返回防止后端未发事件等极端情况下无限等待。技术前置确认方案依赖「前端能单独判断 Leader 的运行时就绪信号」。该信号已从 F2 确认存在——ITeamAgentRuntimeStatusEvent带slot_idstatus见 teamTypes.tsTeamAgentRuntimeStatus 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] useStateTeamWarmupPhase(team_id ? warming : ready); const [runtimeStatus, setRuntimeStatus] useStateMapstring, 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); // stoppedidle 回收停止交由发送框以可恢复提示呈现不触发页面级遮罩 }); 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 若已 readystatusMap初值是否已是非 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 / §3TeamMemberCapsuleBar替换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 新增 keykey用途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.retryteammate 失败态team.addMember.tellLeaderHint/team.addMember.tellLeaderCta/team.addMember.tellLeaderPrefill告诉 Leaderteam.emptyState.leaderGreetingLeader 问候准确翻译 en-US / zh-CN / zh-TW其余 locale 英文兜底改后跑node scripts/generate-i18n-types.jsnode scripts/check-i18n.js脚本位于 scripts/ 目录。10. 分阶段落地与验收每阶段流程改动 →bunx tsc --noEmitoxlint 相关单测 bun run package起 dev/CDP 自查 → 交验收 → 通过后按聚焦 commit。阶段内容独立验收点1身份色系统 胶囊成员栏 气泡区分 选中态高亮多成员下颜色区分清晰、选中态明显、增删成员颜色稳定别人不变色2并行/单聊视图切换按团队记忆两视图切换连贯、按团队记忆、选中态共享、移除回退 Leader3Warmup 状态 禁用清单 失败/超时兜底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_idstatus字段确认存在teamTypes.ts且实现侧额外引入sessionStatusChangedensureSession兜底形成双通道冗余。性能身份色映射为 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 upStar if you like it!项目地址: https://gitcode.com/GitHub_Trending/ai/AionUi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考