Repository navigation
[Bug][desktop] 顶栏会话菜单(标题右侧 ⌄)展开被一次 RPC 阻塞且无加载反馈,点击后表现为"没反应" #519
Description
Product or interface
Desktop app
Version
MiniMax Code Desktop 3.1.4.189 (macOS build)
Platform
macOS
OS version and architecture
macOS 26.3.1 (build 25D2128), arm64
Issue area
UI / UX
Desktop log upload ID (optional)
No response
Steps to reproduce
- 打开 MiniMax Code Desktop,进入任意一个本地会话(非云端会话)。
- 顶栏会话标题右侧有一个
⌄箭头按钮(data-testid="chat-topbar-session-menu-trigger")。 - 点击该箭头。
观察两点:
- 触发方式没有提示:鼠标悬浮在箭头上只会有一个浅灰背景和图标颜色变化,不会弹出菜单;必须点击才会展开。全程没有任何 tooltip / 文案说明这一点。
- 点击后不是立即响应:点击和菜单真正出现之间,存在一段明显可感知的"空窗期"(空窗期长短取决于一次本地 RPC 的耗时)。本地 runtime 正在跑任务、刚启动尚未就绪、或会话尚未注册时,这个空窗期会被显著拉长,表现就是"点了没反应";连点几次反而更糟。
Expected and actual behavior
Expected: 点击箭头后菜单应立即展开(至多对"复制为新会话 / 新工作树"这两项显示加载态),且悬浮时有 tooltip 说明这是可点击的下拉菜单。
Actual:
- 打开动作被一次异步 RPC 整体阻塞。 菜单不是点击即开,而是要先等一次
GetSessionForkOptionsRPC 返回后才展开;等待期间界面零反馈。 - 等待期没有任何加载/禁用/旋转等可见反馈,按钮只有
aria-label(zh:会话操作),没有title、没有 tooltip、没有aria-expanded,所以用户既不知道触发方式,也不知道系统是否已经收到点击。 - 重复点击会互相作废。 打开流程用了一个自增计数器做守卫,加载期间再次点击会让前一次待处理的"打开"失效,菜单要等最后一次请求返回才出现 —— 越点越慢,主观感受就是"按钮坏了"。
- UI 层没有超时与降级。 整个 gate 没有
Promise.race超时兜底,本地 runtime 忙或未就绪时,点击可以长时间表现为完全无响应。 - Promise 链没有
.catch()。 目前底层loadMenuOptions内部吞掉了异常所以不会 reject,但一旦该调用链任一环抛出,就会变成 unhandled rejection,菜单永远不打开且没有任何报错提示。 - 可发现性缺失 + 目标语义冲突。 箭头命中区只有 30×30;而点击标题文字(
data-testid="chat-topbar-title-button")触发的是内联重命名,不是菜单 —— 最显眼的入口做的是另一件事,真正的主菜单入口却没有任何指引。
Root cause analysis (analyzed from the shipped 3.1.4.189 bundle)
分析对象:
/Applications/MiniMax Code.app/Contents/Resources/app.asar→out/_next/static/chunks/(桌面端 UI 未开源,以下为对已发布产物的静态分析)。
1) 触发方式是"点击",不是"悬浮"。
-
顶栏触发按钮:
chunks/75788.db1775b1813df733.js<Dropdown menu={nI} placement="bottomLeft" compact open={ts} onOpenChange={tL} ...> <button type="button" aria-label={t("chat_panel.session_menu_aria")} // zh: 会话操作 data-testid="chat-topbar-session-menu-trigger" className={eG(ts)}> <ChevronDown size={16} /> </button> </Dropdown>
-
共享 Dropdown 封装(webpack module
13376,位于chunks/90321-e50007b0d4d62264.js):trigger: L = ["click"], // 默认值 ... <AntDropdown trigger={L} open={ei} onOpenChange={es} ... /> // trigger 原样透传给 antd
顶栏调用处没有传
trigger,因此为 click-only。悬浮时的hover:bg-bg_interaction_tertiary_hover只是背景色反馈,不触发展开。
2) 为什么"点了没反应":打开被一次 fork 选项预取整体 gate 住。
顶栏容器(chunks/75788.db1775b1813df733.js):
const [ts, ti] = useState(false); // ts = 菜单是否展开
const tr = useRef(0); // 请求序号
const tF = onMenuOpen; // fork 菜单预取
const tL = useCallback((open) => {
const token = ++tr.current;
if (!open) { ti(false); tF(false); return; }
ti(false); // ① 先关掉
tF(true).then(() => { // ② 等一次异步预取
if (tr.current === token) ti(true); // ③ 才真正展开
});
}, [tF]);tF 来自 fork 菜单 hook(webpack module 82610):
onMenuOpen: async (open) => {
open && enabled && await forkMenuStore.getState().loadMenuOptions({ source, sessionId });
}loadMenuOptions → service.getForkOptions() → thrift DesktopService.GetSessionForkOptions,
GET /minimax-desktop/api/v1/session/:id/fork/options(transport: "metadataOnly"),即一次到本地 runtime 的 RPC。
于是 "展开菜单"被绑死在"预取 fork 选项"上:本地 runtime 越慢,点击后菜单出现得越晚,慢到一定程度就是"完全没反应"。
3) 同一模式也存在于侧边栏会话菜单(chunks/app/(pages)/(mavis)/archon/page-f0f825d0d4c82846.js):
tc = (open) => {
const token = ++eU.current;
if (!open) { eO(false); eE(false); return; }
eO(false); eW(false);
eE(true).then(() => { if (eU.current === token) eO(true); });
};两者共用同一个 fork-menu store,建议一并修复。
4) 其他相关事实
- hook 已经暴露了
loading(以及available/worktreeVisible),但顶栏完全没有使用loading。 - 该 gate 只在本地会话且会话支持 fork 时生效(
enabled && isLocal(source) && !isRemote());云端会话会立即展开 —— 这也解释了为什么这个问题只在本地会话上稳定复现。 - 箭头按钮尺寸为
size-[30px] min-h-[30px] min-w-[30px],展开态只切换bg-bg_interaction_tertiary_selected/text-icon_default_primary;chevron 图标不会旋转,没有aria-expanded。
Proposed fix
按优先级:
- 点击即展开,异步只补数据。
onOpenChange(true)时立刻ti(true);复制为新会话 / 新工作树两项先以 loading 骨架呈现,GetSessionForkOptions返回后再填充或启用。用户感知延迟降为 0。 - 给 RPC 加超时与降级。
Promise.race([rpc, timeout(1500~2000ms)]);超时按canFork=false降级(该项置灰),不再阻塞整个菜单。 - 补可见反馈与可发现性。
- 触发器加
aria-expanded/aria-busy;pending 时显示 spinner 或让 chevron 旋转。 - 加 tooltip(如"会话菜单")明确这是点击触发;悬浮时 chevron 可轻微位移/加深,给出"可点"的暗示。
- 展开态让 chevron 旋转 180°,让开合状态一眼可辨。
- 触发器加
- 修掉重复点击互相作废的竞态。 用 request-id +
AbortController取代自增计数器守卫:旧的请求取消,而不是仅仅被忽略,保证"最后一次点击一定打开"。 - 补
.catch()。 失败时 toast 提示,并且仍然打开菜单(其余菜单项不依赖 fork),避免静默失败。 - 统一修复侧边栏会话菜单,复用同一 hook 与同一套降级逻辑。
- (可选,产品决策) 让标题区域本身承载菜单(如点击标题 = 菜单、双击 = 重命名),或支持右键菜单,减少"标题是重命名、箭头才是菜单"的语义冲突。
Redacted error summary
No response
Screenshots
(附:顶栏标题与 ⌄ 按钮、以及展开后的会话菜单截图)
附:给维护者的简短结论
- 触发方式:点击(不是悬浮)。
module 13376的trigger默认["click"],顶栏未覆盖。 - "不灵敏"的根因:菜单展开被
loadMenuOptions()(一次到本地 runtime 的 thrift RPC)整体 gate 住,且 UI 层无超时、无 loading 反馈、无 catch,并带一个会让重复点击互相作废的计数器守卫。 - 最小可行修复:先展开菜单 + fork 项 loading 态,再补超时降级与
.catch()。
Activity