Skip to content

Commit 5f62664

Browse files
BinLiang2021claude
andcommitted
docs(dashboard): backfill mirror md for v2 → v2.1.2 code (铁律 #10 debt cleanup)
Fixes today's documented doc debt. Per CLAUDE.md 铁律 #10: "对 .py/.tsx/.ts/.rs 做行为性修改时,必须...更新 md。新增代码文件 → 同一 commit 新增对应 mirror md" — violated across 6 commits today. This commit settles that debt. New mirror md (13 files created over today's dev but mirror never landed): Backend: backend/routes/_dashboard_schema.md backend/routes/_dashboard_helpers.md backend/routes/_rate_limiter.md Frontend dashboard components: AttentionBanners.md · JobsSection.md · MetricsRow.md · QueueBar.md RecentFeed.md · SessionSection.md · Sparkline.md · DashboardSummary.md expandState.md · healthColors.md Updated mirror md (stubs promoted to full intent + stale references fixed): frontend/src/components/dashboard/ AgentCard.md — stub → full (rewritten 3 times in v2/v2.1/v2.1.1/v2.1.2; documents OwnedCard/PublicCard split as permission boundary, expand semantics, stopPropagation contract, rail-dim behavior) ConcurrencyBadge.md — stub → full (v2.1.1 removed owned branch; reasoning for "×N" confusion + public bucket as privacy measure) StatusBadge.md — stub → full (8-kind → icon/color table + extension guide) DurationDisplay.md — stub → full (Date.now() purity exception + polling tick refresh cadence trade-off) frontend/src/pages/DashboardPage.md — updated render section to reflect v2.1.1+ structure (AgentCard self-manages expand, AgentCardExpanded removed, DashboardSummary added); added single-expandedId strategy note Dead code removal: frontend/src/components/dashboard/AgentCardExpanded.tsx — orphaned since v2.1.1 retire; DashboardPage.tsx already explicitly did not render it. Removed file + its mirror md in this commit per 铁律 #10 ("删除代码文件 → 同一 commit 删除对应 mirror md"). Stale "intentionally NOT rendered" comment in DashboardPage.tsx also removed. Not in this commit (acknowledged gaps, separate cleanup): - Tier-3 reference (`.mindflow/project/references/dashboard_system.md`) — dashboard is now substantial enough to warrant one, but user scoped this task to "补文档" (fix debt), not "add tier-3". Separate followup. - pre-commit hook to mechanically enforce 铁律 #10 going forward. Same followup. - last_verified timestamp refresh on mirrors whose underlying code was modified but intent didn't change (active_sessions, dashboardStore, lib/tauri, commands/tray, backend/main, backend/auth, backend/routes/websocket). Their intent descriptions remain accurate; leaving timestamps alone rather than rubber-stamping. tsc -b: exit 0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2799a8e commit 5f62664

22 files changed

+577
-137
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
code_file: backend/routes/_dashboard_helpers.py
3+
last_verified: 2026-04-13
4+
stub: false
5+
---
6+
7+
# backend/routes/_dashboard_helpers.py — Intent
8+
9+
## 为什么存在
10+
`dashboard.py` 路由里所有**可纯函数化**的逻辑抽出来——便于单测、便于复用、也让 route 文件本身保持薄。
11+
12+
覆盖:action_line 构造、排序、kind 分类、分桶、to_response factory、4 个 async 聚合 fetcher、sparkline 查询、health 派生、humanize verb、banner 派生、recent events 整形。
13+
14+
**职责明确界限**:这里****做"形式转换 + DB 查询 + 字符串组装",不做 HTTP、不做 auth、不做 rate limiting。Route 层负责调度。
15+
16+
## 上下游
17+
- **上游**`backend/routes/dashboard.py` 的主路由 + lazy 详情路由都调这里的函数
18+
- **下游**
19+
- `xyz_agent_context.utils.db_factory.get_db_client` 做所有 DB 查询
20+
- `backend/state/active_sessions.py::get_session_registry` 只被 route 直接调用(这里不碰 registry)
21+
- `backend/routes/_dashboard_schema.py` 所有 Pydantic 类型
22+
- **测试**`tests/backend/test_dashboard_helpers.py`(纯函数)+ `test_dashboard_fetchers.py`(async DB)+ `test_dashboard_v21.py`(v2.1 additions)
23+
24+
## 设计决策
25+
1. **`build_action_line` 不走 events.embedding_text**(TDR-4 + R11):那是 Step 4 持久化产物,对**正在运行**的 event 大概率 null。改走 `instance_jobs.description` / `bus_messages.content` / `sessions[0].channel` 等实时字段。
26+
2. **`sort_agents` 分两组**(TDR-11):Running 组(按 started_at desc)在前,Idle 组(按 last_activity_at desc)在后。None 时间戳当作最老(空字符串 lexicographical 最小)。
27+
3. **`humanize_verb` v2.1.2 加 `instances` 参数**:CALLBACK/SKILL_STUDY/MATRIX 三种 kind 不能再返回硬编码字符串,必须用 `module_class + description`。否则用户看不出 agent 在做什么模块。
28+
4. **`fetch_jobs` 返回 RAW per-state lists**(v2.1.1 bug 修复):不再有 `pending` union 字段。每个 state 的 list 互不重叠,调用方可以放心遍历所有 state 不会 double-count。
29+
5. **`fetch_enhanced_signals.token_rate_1h` 硬返 None**:events 表当前没有 per-event token 列。要真出数字需要先扩 schema。前端遇到 null 渲染 "N/A" 而不是 0(避免误导)。
30+
6. **`derive_health` 优先级**:error > warning > paused > healthy_running > idle_long > healthy_idle。这个顺序直接决定 rail 颜色;改顺序前看清楚 TDR-4 + 前端 HEALTH_COLORS。
31+
7. **`bucket_count` 的范围**(0 / 1-2 / 3-5 / 6-10 / 10+):和 `_dashboard_schema.CountBucket` Literal **必须同步**
32+
33+
## Gotcha
34+
- 所有 SQL 查询用 `%s` 占位符(MySQL 风格),`AsyncDatabaseClient.execute` 会自动翻译成 sqlite `?`。混用会崩。
35+
- `fetch_last_activity` 返回的 `last_at` 如果后端是 MySQL,值是 `datetime` 对象不是 ISO string,**必须调用方自己 `_iso()` 归一化**(route 层已处理)。
36+
- `derive_health``dateutil.parser` 解析 ISO 时间——如果 events.created_at 是 naive datetime(无时区),会当 UTC 处理。如果以后改 MySQL 列为带时区,要 review。
37+
- `build_run_state_for_agent` 对 sessions 做"取第一个"——真实生产中若 session 排序需求变化(比如按最新优先),要改。
38+
- `_LIVE_JOB_STATES` 元组顺序**不是**展示顺序,是 SQL `WHERE status IN (...)` 的参数顺序;前端展示顺序在各自的组件里。
39+
- `humanize_verb` 对未知 `kind` 返回 `Running ({kind})` 作为最后 fallback——这意味着后端若加了新 `WorkingSource` 但忘了更新这个函数,前端会看到字面 `Running (NEW_KIND)`,不崩但难看。加 kind checklist:改这里 + StatusBadge ICON_MAP + types/api.ts `AgentKind` union。
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
code_file: backend/routes/_dashboard_schema.py
3+
last_verified: 2026-04-13
4+
stub: false
5+
---
6+
7+
# backend/routes/_dashboard_schema.py — Intent
8+
9+
## 为什么存在
10+
Pydantic 响应类型的**唯一真相源**(SSOT)for `GET /api/dashboard/agents-status`
11+
12+
关键职责:**用类型系统把权限边界焊死**——owner-only 字段不能出现在 public 变体上,由 Pydantic `extra='forbid'` + `Literal[True/False]` discriminated union 强制。即使 `to_response` factory 写漏,validation 层拒绝序列化。
13+
14+
## 上下游
15+
- **上游**`backend/routes/_dashboard_helpers.py::to_response`(factory)、`backend/routes/dashboard.py` 路由响应
16+
- **下游**:前端 `frontend/src/types/api.ts` 里手工复刻了同样的类型(TS 侧,Pydantic → TS 没自动化生成;drift 风险见 Gotcha)
17+
- **平行**`frontend/src/types/api.ts``OwnedAgentStatus / PublicAgentStatus` 必须和这里**字段-by-字段**对齐
18+
19+
## 设计决策
20+
1. **Discriminated union via `owned_by_viewer`**`Literal[True]``Literal[False]` 才是真 discriminator;普通 `bool` 字段 + 默认值在 Pydantic v2 + FastAPI response_model 里不 work。序列化出错会很隐蔽,所以必须 Literal。
21+
2. **`ConfigDict(extra='forbid')` on PublicAgentStatus**:防御性措施。Factory 里写 `sessions=[]` 传给 public 会在 validation 就 raise,而不是序列化成功然后泄漏。
22+
3. **`running_count_bucket` 替代精确数字** on public 变体(TDR-13):侧信道防御,防止通过流量分析识别大客户 agent。
23+
4. **v2.1 新增的 owner-only 字段**都在 `OwnedAgentStatus` 内:`verb_line / queue / recent_events / metrics_today / attention_banners / health`。Public 变体**刻意不含**这些。
24+
5. **`action_line: str | None`** 而不是空串——`null` 让前端能明确渲染 ``
25+
26+
## Gotcha
27+
- **TS 类型手工复刻**:后端加字段若忘了同步 `frontend/src/types/api.ts``tsc` 不报错(TS 对多余字段宽容)。没有自动化契约测试。加字段 checklist:改这里 → 改 types/api.ts → 跑 tsc。
28+
- `running_count_bucket` 的字面值列表(`'0' | '1-2' | '3-5' | '6-10' | '10+'`)和 `_dashboard_helpers.py::bucket_count` 的输出是**隐式耦合**——改其中一个必须改另一个。
29+
- `JobQueueStatus` 的 5 个值和 `_dashboard_helpers._LIVE_JOB_STATES`(去掉 `running`**必须对齐**——改枚举两处都要动。
30+
- 字段添加到 `OwnedAgentStatus` 时若是 owner-only:务必用 `ConfigDict(extra='forbid')` 保护 `PublicAgentStatus`(已做),并在 `tests/backend/test_dashboard_v21.py::test_v21_public_variant_still_locked_down``forbidden` 集合里加上新字段名,否则白名单漏检。
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
code_file: backend/routes/_rate_limiter.py
3+
last_verified: 2026-04-13
4+
stub: false
5+
---
6+
7+
# backend/routes/_rate_limiter.py — Intent
8+
9+
## 为什么存在
10+
Dashboard 端点每 3s polling,合法用户流量约 0.33 req/s。但恶意用户开 100 个 tab 就是 33 req/s,每个请求扇出 4+ DB 查询——需要一个**最低限度**的 per-viewer 限流(security critic rev-1 M-5)。
11+
12+
不上 `slowapi` / Redis——单进程 in-memory sliding window 对单 worker 部署刚好够。
13+
14+
## 上下游
15+
- **上游**`backend/routes/dashboard.py` 的主路由和每个 lazy 详情路由(job/session/sparkline/retry/pause/resume)都在入口调 `SlidingWindowRateLimiter.allow(viewer_id)`
16+
- **下游**:无(纯内存,无依赖)
17+
- **测试**`tests/backend/test_rate_limiter.py` 4 个单测(limit、window 恢复、key 隔离、idle cleanup)
18+
19+
## 设计决策
20+
1. **deque 而非 list**`popleft` O(1)、`append` O(1)。列表 `pop(0)` 是 O(n),在窗口有很多旧时间戳时慢。
21+
2. **滑动窗口**(不是固定窗口或 token bucket):
22+
- 固定窗口容易被"窗口边界突发"绕过(恰好跨窗口的两次爆发)
23+
- Token bucket 实现复杂、需要 refill rate——对我们 2 req/s 的粗糙要求过设计
24+
- Sliding window:每次请求清理 `< now - window_sec` 的旧条目 + 看 len
25+
3. **Idle cleanup**(v2.1 security NC-2):每 `cleanup_interval` 次请求扫一次 `_deques` dict,删空 deque 的 key。防止长期运行后 dict 无限增长。
26+
4. **monotonic time 而非 wall clock**:防止系统时间漂移或 NTP 调整扰动窗口。
27+
5. **单实例 per router**:route 模块顶层构造 `_rate_limiter = SlidingWindowRateLimiter(limit=2, window_sec=1.0)`,所有 endpoint 共享一个——每个 viewer 的 budget 跨端点共用,不是每端点各 2 req/s。
28+
29+
## Gotcha
30+
- **进程级内存**:多 worker 部署(`WEB_CONCURRENCY>1`)每个 worker 独立计数——一个 viewer 总限流变成 `2 × N workers`。和 `active_sessions` 一样的单进程假设;`backend/main.py::_warn_if_multi_worker` 会启动时 warn。若真上多 worker 要切 Redis。
31+
- **测试时 window_sec 要 patch 大**:真实 DB 测试每请求可能 >500ms,几个请求就跨窗口了,触发不到 429。`test_dashboard_route.py::test_rate_limit_returns_429_on_burst` 里 monkeypatch `_window=3600.0`
32+
- `allow()` 返 bool;route 层负责 raise `HTTPException(429, headers={"Retry-After": "1"})`——**必须带 headers 参数**,普通 `HTTPException` + `response.headers["Retry-After"]=...` 在 FastAPI 里不生效(response 对象会被 exception 路径丢弃)。
33+
- `cleanup_interval` 默认 100——意味着前 100 请求都不清理。低流量长时间运行下这个 dict 会累积。可接受但别忘了。

.mindflow/mirror/backend/routes/dashboard.md

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,45 @@ last_verified: 2026-04-13
44
stub: false
55
---
66

7-
# backend/routes/dashboard.py
7+
# backend/routes/dashboard.py — Intent
88

99
## 为什么存在
10-
`GET /api/dashboard/agents-status` 端点。Dashboard v2 前端的唯一后端入口。聚合 events + jobs + module_instances + active_sessions + narratives 一次响应返回;按 viewer 权限裁剪;按 Running/Idle 分组倒序排序
10+
`GET /api/dashboard/agents-status` 端点 + v2.1/v2.1.1/v2.1.2 新加的一组懒加载详情端点和 job mutation 端点。Dashboard 前端所有 HTTP 调用的唯一后端入口
1111

12-
v1 已推倒重写(见 archive/2026-04-13-dashboard-v1/)。
12+
v1 已推倒重写(归档在 `.mindflow/state/archive/2026-04-13-dashboard-v1/`)。
13+
14+
## 端点清单(v2.1.2)
15+
- `GET /api/dashboard/agents-status` — 主聚合视图(polling 目标)
16+
- `GET /api/dashboard/agents/{id}/sparkline?hours=24` — 24h events/hour 桶(懒加载)
17+
- `GET /api/dashboard/jobs/{job_id}` — 单 job 全量详情
18+
- `GET /api/dashboard/sessions/{session_id}?agent_id=X` — 单 session 详情
19+
- `POST /api/dashboard/jobs/{id}/retry` — failed/blocked/cancelled → pending
20+
- `POST /api/dashboard/jobs/{id}/pause` — active/pending → paused
21+
- `POST /api/dashboard/jobs/{id}/resume` — paused → pending
1322

1423
## 上下游
15-
- 上游:前端 `frontend/src/lib/api.ts::getDashboardStatus()`(3s/30s 自适应 polling)
16-
- 下游
17-
- `backend/state/active_sessions.py` snapshot(并发会话)
18-
- `backend/routes/_dashboard_helpers.py` 查询 + 组装 helper
19-
- `backend/routes/_dashboard_schema.py` Pydantic 响应类型
20-
- `backend/routes/_rate_limiter.py` 2 req/s per-viewer 限流
21-
- `backend/auth.py::get_local_user_id` / `request.state.user_id`(JWT)
24+
- **上游**:前端 `lib/api.ts` 的 7 个方法一一对应
25+
- **下游**
26+
- `_dashboard_helpers.py` — 组装、查询、派生逻辑
27+
- `_dashboard_schema.py` — Pydantic 响应类型(discriminated union)
28+
- `_rate_limiter.py` — per-viewer 2 req/s 滑窗
29+
- `backend/state/active_sessions.py::get_session_registry` — WS session snapshot
30+
- `backend/auth.py::_is_cloud_mode` / `get_local_user_id` — viewer 身份识别
2231

2332
## 设计决策
24-
- **Pydantic discriminated union**(TDR-5):`PublicAgentStatus` (owned_by_viewer=Literal[False] + extra='forbid') vs `OwnedAgentStatus` (Literal[True])。类型层阻止 owner-only 字段泄漏到 public 响应
25-
- **`?user_id=` 被拒**(TDR-12):viewer_id 永远从 auth 上下文识别,query param 是身份冒充向量
26-
- **asyncio.gather(return_exceptions=False)**(TDR-8):任一聚合 query 失败 → 整个 request 500,不做 partial degradation
27-
- **rate limit 2 req/s**(TDR-6):合法 dashboard polling 是 0.33 req/s,2 req/s 给手动刷新留余量但挡 100-tab DoS
28-
- **排序分组**(TDR-11):Running 组在前(按 started_at desc),Idle 组在后(按 MAX(events.created_at) desc)。`_earliest_started_at` 对并发 session 取 min
33+
1. **viewer_id 永远从 session 读**(TDR-12):cloud 走 JWT (request.state.user_id);local 走 `get_local_user_id()`**拒绝 `?user_id=X`** 返 400——这是 security rev-1 C-1 的修复点。
34+
2. **`asyncio.gather(return_exceptions=False)`**(TDR-8):4 个聚合查询(last_activity / jobs / instances / enhanced)+ v2.1 加的 2 个(recent_events / metrics_today)并发。任一失败整个 request 500——不做 partial degradation(不完整的 dashboard 比错误更误导)。
35+
3. **Pydantic discriminated union 序列化**`AgentStatus = Annotated[Union[Owned, Public], Field(discriminator='owned_by_viewer')]`。Literal[True/False] 让 FastAPI + OpenAPI 生成正确。
36+
4. **Rate limit 在 endpoint 开头**:拒绝到了 DB 层之前——避免恶意流量打穿 DB 池。
37+
5. **`HTTPException(429, headers={"Retry-After": "1"})`**:必须走 exception 的 headers 参数,直接改 response.headers 在 FastAPI 里会被 exception 路径丢掉。
38+
6. **Lazy 端点各自 re-check ownership**`_assert_agent_visible()` + owner-only check。Public 非自有用户**不能**读 job/session 内部。这是 security rev-1 C-2 的延伸——不仅主端点有类型层保护,每个 drill-down 端点也要检权。
39+
40+
## v2.1.1 bug 修复点
41+
`_derive_kind``_earliest_started_at` 保留但 `pending_jobs_items` 构造改了——原来遍历 `per_state["pending"]` 时它是 union(pending+active+blocked+paused),导致后续遍历 "active"/"blocked"/"paused" 双重计算。v2.1.1 `fetch_jobs` 返 RAW per-state 后,本文件加 `seen_job_ids: set` 二保险去重。
2942

3043
## Gotcha
31-
- action_line 数据源:`events.embedding_text` 对 running 态几乎必 null(Step 4 才写),因此 `build_run_state_for_agent``instance_jobs.description` / `bus_messages.content` / session channel 取;fallback "Running (kind)"(TDR-4 已知局限 + R11)
32-
- `HTTPException` 429 必须带 `headers={"Retry-After": "1"}`,否则 FastAPI 丢掉 response.headers 设置
33-
- running_count_bucket 是 public 视角的隐私措施,不是纯展示:精确 int 暴露可被用作流量推断攻击(security M-1)
44+
- **query param 白名单**:目前只 check `user_id`,其他 unknown params 不拒绝。如果未来有敏感 query 要加,显式 reject。
45+
- **`_iso()` 对 datetime 转 ISO**——但不加时区标识。MySQL 返回 naive datetime,ISO 字符串里没 `+00:00`。前端 `new Date(iso)` 会按**浏览器本地时区**解读,和后端本地时区可能不一致。长期:改后端存 timezone-aware 并输出 `+00:00`
46+
- **Lazy 端点的 viewer 识别**走同一 `_resolve_viewer`——保证主端点和 drill-down 语义一致。如果主端点支持某种新身份(如 API key),lazy 端点**不会自动**继承,要显式改。
47+
- **Mutation 端点**(retry/pause/resume)目前直接改 `instance_jobs.status`——绕过了 job_trigger 的调度器逻辑。如果 job_trigger 未来加了"状态变更钩子",这里要 audit 是否漏触发。
48+
- `update_time` column:mutation SQL 用 `datetime('now')` 是 SQLite 方言;MySQL 需要 `NOW()`。AsyncDatabaseClient 的 `_mysql_to_sqlite_sql` 做反向翻译,原始 SQL 写 `datetime('now')` 可能在 MySQL 上出问题——**需要验证**。更稳的是 Python 侧生成 ISO 时间传参。
Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,58 @@
11
---
22
code_file: frontend/src/components/dashboard/AgentCard.tsx
33
last_verified: 2026-04-13
4-
stub: true
4+
stub: false
55
---
66

7-
# AgentCard.tsx
7+
# AgentCard.tsx — Intent
88

9-
## Intent
10-
Dashboard v2 presentational component. See code file docstring for behavior.
11-
See `DashboardPage.md` for orchestration and `dashboardStore.md` for data.
9+
## 为什么存在
10+
Dashboard 网格里每个 Agent 对应一张卡片,这是**唯一**的卡片实现。渐进式披露的**执行者**:folded 时给"身份 + verb + banner + 内联关键数字",点击展开后有完整 session/job/sparkline/recent feed。
1211

13-
## Expand before next iter
14-
Flesh out when next dashboard redesign happens. Current behavior + data contract are stable enough that source docstring + tests suffice.
12+
今天一天内此文件被重写 4 次(v2 → v2.1 → v2.1.1 → v2.1.2),每次响应用户直接反馈,详见 git log。
13+
14+
## 两个子组件(权限分叉)
15+
- **`<OwnedCard>`** 渲染 v2.1 rich 字段(verb_line, attention_banners, queue, sessions, jobs, sparkline, recent_events, metrics_today)
16+
- **`<PublicCard>`** 只渲染 header(name + kind + concurrency bucket + duration)
17+
18+
这是**组件级权限边界**——配合 Pydantic `PublicAgentStatus extra='forbid'` 形成防泄漏双保险。即使后端意外多传字段,前端也不 access,自然不显示。`AgentCard` 本身只根据 `owned_by_viewer` 分派。
19+
20+
## 数据契约
21+
消费 `OwnedAgentStatus` 全部字段。关键点:
22+
- `health` → 左侧 rail 颜色(映射在 `healthColors.ts`
23+
- `verb_line` → 主叙事(humanized,后端 `humanize_verb` 生成)
24+
- `attention_banners` → 可 dismiss 的顶部通知(`<AttentionBanners>`
25+
- `queue` + `metrics_today` → 内联紧凑条(`<QueueBar compact>` + `<MetricsRow>`
26+
- `sessions` / `running_jobs` / `pending_jobs` → 展开后的 section
27+
- `recent_events` → 展开后的折叠 feed
28+
- Sparkline 懒加载(自己 fetch `/agents/{id}/sparkline`
29+
30+
## 交互
31+
- **卡片身 onClick = toggle expand**`onToggleExpand` prop,上游 `DashboardPage` 维护 `expandedId`
32+
- 所有内部交互元素(banner `[×]`、section header、item rows、action buttons)都 `e.stopPropagation()` — 不冒泡触发卡片展开
33+
- `role="button"` + `tabIndex={0}` + `onKeyDown` (Enter/Space) → 键盘可达
34+
- `▾ more / ▴ less` 只是视觉提示,不再是按钮——整张卡就是按钮
35+
36+
## v2.1.2 新行为:rail dim
37+
`useAllBannersDismissed(keys)` 读 sessionStorage 判定是否所有 banner 都被 dismiss。是 → rail + card tint 加 `opacity-40`。语义:"用户已经 acknowledge 所有告警 → 视觉降级到安静状态"。新 banner 出现(signature 变)自动 un-dim。
38+
39+
## 依赖关系
40+
```
41+
AgentCard
42+
├── StatusBadge, DurationDisplay, ConcurrencyBadge (header)
43+
├── AttentionBanners (dismissible)
44+
├── SessionSection → SessionItem (lazy session detail)
45+
├── JobsSection → JobItem (lazy job detail + retry/pause/resume)
46+
├── QueueBar (compact mode + full mode)
47+
├── Sparkline (独立 fetch)
48+
├── RecentFeed (collapsible)
49+
├── MetricsRow
50+
└── healthColors (palette) + expandState (useExpanded + useAllBannersDismissed)
51+
```
52+
53+
## Gotcha
54+
- **`AgentCardExpanded.tsx` 已被移除**(v2.1.1 retire)。v2 时用作外置展开容器,v2.1 起卡片自管展开,外置容器冗余。如果在别处看到引用——历史遗迹,删除即可。
55+
- **`running_count` 不是 ConcurrencyBadge 的显示依据(owned)**。v2.1.1 起 owned 完全不渲染 ConcurrencyBadge——`verb_line` 给出类型 + 数量,比孤立 `×N` 清楚。
56+
- **`stopPropagation` 是强契约**:新加内部交互元素必须显式 stopPropagation,否则意外触发卡片展开。eslint 不保护,靠人盯。
57+
- **`expandedId` 同时只展开一张**`DashboardPage` 状态)。想支持多张同时展开需改页面级状态。
58+
- **`idle_long` opacity-75 + banner-dismissed opacity-40 可叠加**——两者都 true 时视觉非常淡,符合"不打扰"意图。

0 commit comments

Comments
 (0)