Skip to content

[Bug][desktop] 顶栏会话菜单(标题右侧 ⌄)展开被一次 RPC 阻塞且无加载反馈,点击后表现为"没反应" #519

Open

Description

@guaiguaidashu

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

  1. 打开 MiniMax Code Desktop,进入任意一个本地会话(非云端会话)。
  2. 顶栏会话标题右侧有一个 ⌄ 箭头按钮(data-testid="chat-topbar-session-menu-trigger")。
  3. 点击该箭头。

观察两点:

  • 触发方式没有提示:鼠标悬浮在箭头上只会有一个浅灰背景和图标颜色变化,不会弹出菜单;必须点击才会展开。全程没有任何 tooltip / 文案说明这一点。
  • 点击后不是立即响应:点击和菜单真正出现之间,存在一段明显可感知的"空窗期"(空窗期长短取决于一次本地 RPC 的耗时)。本地 runtime 正在跑任务、刚启动尚未就绪、或会话尚未注册时,这个空窗期会被显著拉长,表现就是"点了没反应";连点几次反而更糟。

Expected and actual behavior

Expected: 点击箭头后菜单应立即展开(至多对"复制为新会话 / 新工作树"这两项显示加载态),且悬浮时有 tooltip 说明这是可点击的下拉菜单。

Actual:

  1. 打开动作被一次异步 RPC 整体阻塞。 菜单不是点击即开,而是要先等一次 GetSessionForkOptions RPC 返回后才展开;等待期间界面零反馈。
  2. 等待期没有任何加载/禁用/旋转等可见反馈,按钮只有 aria-label(zh:会话操作),没有 title、没有 tooltip、没有 aria-expanded,所以用户既不知道触发方式,也不知道系统是否已经收到点击。
  3. 重复点击会互相作废。 打开流程用了一个自增计数器做守卫,加载期间再次点击会让前一次待处理的"打开"失效,菜单要等最后一次请求返回才出现 —— 越点越慢,主观感受就是"按钮坏了"。
  4. UI 层没有超时与降级。 整个 gate 没有 Promise.race 超时兜底,本地 runtime 忙或未就绪时,点击可以长时间表现为完全无响应。
  5. Promise 链没有 .catch()。 目前底层 loadMenuOptions 内部吞掉了异常所以不会 reject,但一旦该调用链任一环抛出,就会变成 unhandled rejection,菜单永远不打开且没有任何报错提示。
  6. 可发现性缺失 + 目标语义冲突。 箭头命中区只有 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

按优先级:

  1. 点击即展开,异步只补数据。 onOpenChange(true) 时立刻 ti(true);复制为新会话 / 新工作树 两项先以 loading 骨架呈现,GetSessionForkOptions 返回后再填充或启用。用户感知延迟降为 0。
  2. 给 RPC 加超时与降级。 Promise.race([rpc, timeout(1500~2000ms)]);超时按 canFork=false 降级(该项置灰),不再阻塞整个菜单。
  3. 补可见反馈与可发现性。
    • 触发器加 aria-expanded / aria-busy;pending 时显示 spinner 或让 chevron 旋转。
    • 加 tooltip(如"会话菜单")明确这是点击触发;悬浮时 chevron 可轻微位移/加深,给出"可点"的暗示。
    • 展开态让 chevron 旋转 180°,让开合状态一眼可辨。
  4. 修掉重复点击互相作废的竞态。 用 request-id + AbortController 取代自增计数器守卫:旧的请求取消,而不是仅仅被忽略,保证"最后一次点击一定打开"。
  5. 补 .catch()。 失败时 toast 提示,并且仍然打开菜单(其余菜单项不依赖 fork),避免静默失败。
  6. 统一修复侧边栏会话菜单,复用同一 hook 与同一套降级逻辑。
  7. (可选,产品决策) 让标题区域本身承载菜单(如点击标题 = 菜单、双击 = 重命名),或支持右键菜单,减少"标题是重命名、箭头才是菜单"的语义冲突。

Redacted error summary

No response

Screenshots

(附:顶栏标题与 ⌄ 按钮、以及展开后的会话菜单截图)


附:给维护者的简短结论

  • 触发方式:点击(不是悬浮)。module 13376 的 trigger 默认 ["click"],顶栏未覆盖。
  • "不灵敏"的根因:菜单展开被 loadMenuOptions()(一次到本地 runtime 的 thrift RPC)整体 gate 住,且 UI 层无超时、无 loading 反馈、无 catch,并带一个会让重复点击互相作废的计数器守卫。
  • 最小可行修复:先展开菜单 + fork 项 loading 态,再补超时降级与 .catch()。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    desktopMiniMax Code desktop app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions