Skip to content

MUL-4884: fix(issues): anchor the working chip on issues the filter actually shows#5540

Open
NevilleQingNY wants to merge 2 commits into
mainfrom
walt/mul-4884-issue-anchored-chip
Open

MUL-4884: fix(issues): anchor the working chip on issues the filter actually shows#5540
NevilleQingNY wants to merge 2 commits into
mainfrom
walt/mul-4884-issue-anchored-chip

Conversation

@NevilleQingNY

Copy link
Copy Markdown
Collaborator

Fixes MUL-4884.

方案 A(issue 锚定),按 naiyuan 的风格约束实现。主 chip 只说一件事:「N 个 issue 进行中」,N = 点击后列表剩下的行数

根因回顾

芯片把三个口径并排摆在一起:数字数 issue、头像堆数 agent(还带一个抢戏的 +N)、悬浮卡数 task,而标签「工作中」没有主语。三个数各自都没算错,凑在一起就读成"算错了"。成因是 c4209ec7c 只把数字从 agent 口径改成 issue 口径,另外三个面留在旧口径。

数据层三项修复

1. 幽灵 issue(issue_id === ""

packages/core/types/agent.ts:248 写明 chat/autopilot 的 task issue_id空字符串(不是 null)。原来 new Set(running.map(tk => tk.issue_id)) 把它们全塌成一个 "" 桶 → 只要有 ≥1 个这类 running task,计数恒定多 1,且这个桶点开筛选匹配不到任何行。

现在在 deriveWorkingChipView 里显式分桶(参考 surface/activity.ts:63 的写法),不计入数字,并在悬浮卡里单独标注"不计入"——不静默丢弃,也不混进 issue 分组。

2. 计数改用过滤后的可见集合

取舍:没有把"过滤后的集合"当参数传给芯片,而是让芯片彻底不再自己派生这个数。

新增 workingScopeIssuesuse-issue-surface-data.ts):把渲染列表的那条 pipeline 原样跑一遍、只把 workingOnly 强制打开。它和列表用的是同一个 applyIssueFilters、同一份 filter state、同一个数据源。

这样 "芯片数字 = 列表行数"是构造性成立的,不是靠约定维持的——打开筛选只是往同一条 pipeline 上加 workingOnly,所以无论筛选当前开还是关,这个集合就是"点击后的列表"。

按视图模式分支,镜像渲染分支:gantt 用自己的 scheduled-only 投影,assignee board 从 groups 走,swimlane 不吃 status 筛选。

3. 分页天花板(ISSUE_PAGE_SIZE = 50)——选了「标注」而不是「去掉分页影响」

取舍理由:这两个目标是冲突的,只能二选一。

列表每个 status 只取首页 50 条。如果把计数改成"不受分页影响"(直接从 snapshot 全量数),数字就会包含点击后根本显示不出来的行——那恰好打破方案 A 的核心承诺(数字 = 点击结果)。

所以计数继续锚定列表(相对于承诺,它是准确的:点击后确实就这些行),漏掉的部分在悬浮卡里显式说出来:

2 more tasks are outside the current filters or loaded range — not counted

这条同时覆盖"被用户筛选条件排除"和"在已加载页之外"两种情况——对用户来说都是"不在你现在看的范围里",没必要区分成两条。零的时候不显示。

如果之后要真正消灭这个缺口,正确的解法是服务端出一个 workspace 级的 running-issue 计数接口,不在这个 PR 的范围里。已知技术债:计数范围受每状态 50 条首页限制,目前靠悬浮卡标注而非消除。

UI(方案 A + 风格约束)

  • 头像堆去掉 +N:新增 overflow="fade",超出 3 个时最后一个头像淡出,精确名单在悬浮卡。其他两个调用方(issue-agent-header-chip / issue-agent-activity-indicator)默认仍是 count,行为不变。
  • 标签自带主语 + 单位:N 个 issue 进行中。窄屏只显示数字,但 aria-label 始终是完整句子。
  • 悬浮卡:卡头两个口径(3 个 issue 进行中 · 4 个 task)+ 按 issue 分组的行;只在非零时追加最多两条 footnote。没有统计卡片、没有图标堆砌。
  • dark mode 蓝色分两层:未筛选有活动 = border-brand/30 bg-brand/5(弱蓝提示),筛选开启才 bg-brand filled。有测试钉住这个契约。
  • 文案按 conventions.zh.mdx:UI 短句里 task/issue 保持小写英文(与相邻的 hover_header_tasks 一致),全角标点。注意:Lalo mock 里写的是「4 个任务」,我按 conventions 用了「4 个 task」。如果 naiyuan 更想要「任务」,改 4 个 locale 的一行即可。
  • filter_active_label 原文案「正在查看工作中的智能体」说的是 agent,但这个筛选过滤的是 issue —— 一并改成「正在查看进行中的 issue」。
  • 移除了 chip_label(无主语的「工作中」),4 个 locale 同步。

验证

  • pnpm exec tsc --noEmit(views):0 error。
  • pnpm exec vitest run(views 全量):215 files / 2182 tests 全绿,含 i18n parity(en/zh-Hans/ja/ko 四语同步)。
  • lint:0 error;改动文件没有新增 warning(issue-surface.tsx 那条 unused eslint-disable 在 origin/main 上就有)。

端到端钉住核心承诺的新测试(use-issue-surface-controller.test.tsx,跑真实 controller + 真实 query pipeline,只 mock API):

  • 筛选打开时 workingScopeIssuesissues(渲染行)逐项相等
  • 筛选关闭时,数字提前等于"点击后会剩下的那一行";
  • status 筛选会同步收窄芯片(这正是原来的 bug:两个 issue 都有 running agent,芯片说 2,点开只有 1 行);
  • showSubIssues 关闭时,只跑在子 issue 上的活不计入(否则芯片说 1、点开 0 行);
  • issue_id === "" 的 chat task 不产生幽灵行。

外加芯片/悬浮卡层的单元测试:截图同款场景(4 task / 4 agent / 3 issue)渲染成「3 issues in progress」+ 无 +N;空 issue_id 分桶;颜色两层契约;中英文案。

未做的验证:没有起真实 backend + daemon 跑一遍界面,所以视觉呈现(间距、淡出观感、实际 dark mode 蓝)没有截图确认——数字契约是靠上面的 controller 级测试证明的,样式部分建议 naiyuan 本地扫一眼。

🤖 Generated with Claude Code

…ows (MUL-4884)

The header chip showed three units at once: the number counted distinct
issues, the avatar stack counted agents (with a rival "+N"), and the hover
card counted tasks — under a label with no noun at all. Each figure was
self-consistent; together they read as a miscount. c4209ec flipped the
number from agents to issues but left the other three surfaces on their old
units.

The chip now says one thing — "N issues in progress" — where N is the number
of rows clicking it leaves.

Data layer:

- The count is no longer re-derived from the task snapshot. The surface
  exposes `workingScopeIssues`: the render pipeline's own output with
  `workingOnly` forced on, so "chip count === row count" holds by
  construction instead of by convention. It previously counted running
  issue_ids against the PRE-filter set, so any active status/assignee/label
  filter — or a sub-issue hidden by the display toggle — made the chip
  disagree with the list it was filtering.
- Chat/autopilot tasks carry issue_id "" (not null). They used to collapse
  into one bucket and read as a phantom issue, inflating the count by exactly
  one whenever any were running. They are now bucketed out and disclosed.
- The list loads one page per status (50), so running work can exist past the
  window. The count stays list-anchored — counting rows a click cannot show
  would break the chip's whole promise — and the gap is stated in the hover
  card rather than dropped silently.

UI:

- Avatar stack is ambience, not a statistic: no "+N"; the tail fades and the
  exact roster lives in the hover card.
- Hover card groups rows by issue, names both counted units in its header
  ("3 issues in progress · 4 tasks"), and footnotes anything excluded — only
  when non-zero.
- Colour is two-step: idle activity is a brand tint, the filled state is
  reserved for "filter is ON".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
multica-docs Ready Ready Preview, Comment Jul 16, 2026 12:13pm

Request Review

…tually draw (MUL-4884)

Review found two modes where `workingScopeIssues` still came from a
different set than the one on screen — the exact drift this change set is
meant to remove.

Swimlane: scoped to the statusless `swimlaneIssues`, but SwimLaneView draws
its cards from `issues` (status filter applied) and only uses the statusless
set for LANE DISCOVERY. A status-filtered swimlane counted rows the canvas
never drew. Swimlane needs no branch at all — it renders the flat filtered
list like board and list do — so the special case is gone rather than
corrected.

Gantt: the canvas applies two rules of its own before drawing — a row needs a
date to be placed, and done/cancelled hide unless `ganttShowCompleted` is on
— so a done issue with a running agent counted toward the chip while the
canvas refused to draw it.

Those rules now live in the surface (`ganttCanvasRows`) instead of privately
inside GanttView, and both `filteredGanttIssues` and the working scope go
through them. Mirroring the rules in a second place would have reproduced the
original bug with extra steps; hoisting them means the chip narrows the same
set the canvas draws, by construction. GanttView goes back to being a
renderer: it orders rows, it does not decide which ones exist.

Regression tests cover both, and each was confirmed to fail against the code
it guards: swimlane asserts the statusless lane source still holds the wider
set, so a revert fails loudly rather than passing by luck; gantt covers the
hidden done row, the undated row, and the show-completed toggle widening both
the canvas and the scope together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant