dsh-agent-board 1.7.6 → 1.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/index.mjs +4 -2
- package/lib/client/board-list.js +35 -16
- package/lib/client/dashboard.js +15 -9
- package/lib/client/kernel.js +41 -9
- package/lib/client/task-detail.js +81 -1
- package/lib/client.js +172 -35
- package/lib/core.mjs +124 -1
- package/lib/dispatch.mjs +43 -7
- package/lib/health.mjs +22 -0
- package/lib/notify.mjs +23 -1
- package/lib/rpc.mjs +143 -15
- package/lib/store.mjs +139 -37
- package/package.json +1 -1
- package/test/archive-detail.test.mjs +43 -0
- package/test/core.test.mjs +204 -17
- package/test/headless-smoke.test.mjs +25 -0
- package/test/helpers/mock-ctx.mjs +17 -3
- package/test/review-hints.test.mjs +170 -0
- package/test/scoreboard-trends-client.test.mjs +21 -0
- package/test/store-data-safety.test.mjs +217 -0
- package/test/verify-timing.test.mjs +292 -0
package/README.md
CHANGED
|
@@ -6,7 +6,25 @@
|
|
|
6
6
|
[](https://github.com/PPawnsir/task-board-plugin/blob/main/LICENSE)
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
智能看板插件 —
|
|
9
|
+
智能看板插件 — agent 团队的持久台账与治理层:把「派发 → 执行 → 验收 → 度量 → 复盘」跑成全链路闭环,每一张卡都是可审计、可度量的长期资产。
|
|
10
|
+
|
|
11
|
+
## 定位
|
|
12
|
+
|
|
13
|
+
**dsh-agent-board 是 agent 团队的持久台账与治理层——看板只是界面,内核是「派发 → 执行 → 验收 → 度量 → 复盘」的全链路闭环。** 它与「一次性组队、跑完即散」的冲锋型编排器、「人建卡、agent 认领、人逐张验收」的手工看板不是一个代际:
|
|
14
|
+
|
|
15
|
+
- **持久台账**:任务卡是长期资产。完成 / 驳回 / 歧义裁决 / 验收结论 / Token 消耗 / 自测指南,每一笔都落账留痕,看板文件即审计底稿;卸载不删数据,重启自动继承。
|
|
16
|
+
- **池化 Worker + 独立 Verifier**:执行与验收分离——Worker 负责动手(可续跑,续跑只认新增产出),Verifier 独立复跑验收脚本、以实证为准,不听 Worker 的一面之词。
|
|
17
|
+
- **独立验收**:`acceptance` 硬性脚本命令由 Worker 实跑、Verifier 独立复跑;结论分通过 / 驳回并逐条给证据。人只看该看的——风险队列「待你过目」把该亲自确认的卡推到眼前,其余交给机器闭环。
|
|
18
|
+
- **可运维度量栈**:记分卡(模型表现 / 质量趋势 / 驳回聚类)、运行时健康自检、异常驱动审视(风险队列 / 质量异动告警 / 批次摘要)三层,让成本与质量从「感觉」变成「数字」。
|
|
19
|
+
|
|
20
|
+
> **流水线归机器,审计台归人。** 我们不造「跑完就散」的冲锋队,也不做「人逐张盯着验收」的手工看板;我们的信念只有三条:**验收独立复跑、人只看该看的、数据沉淀成资产**。别人负责冲锋,agent-board 负责记账与验收——互补,不对抗。
|
|
21
|
+
|
|
22
|
+
### 适合谁 / 什么时候用
|
|
23
|
+
|
|
24
|
+
- **长期多任务并行**:一个会话里几十张卡、有依赖有先后、还不断追加新需求——需要一份不随会话结束而消失的台账,而不是每次重开都从零描述。
|
|
25
|
+
- **关心成本与质量趋势**:想看清哪个模型在什么场景更省、驳回率在涨还是跌、哪些文件反复成为锁热点——需要记分卡与趋势,而不是拍脑袋。
|
|
26
|
+
- **不想逐张审卡**:接受「独立复跑 + 风险队列兜底」的治理方式——机器把「该我亲自看」的卡筛出来,其余信任验收闭环。
|
|
27
|
+
- **缺一层治理而非缺一个编排器**:已有冲锋型的团队 / 编排工具,缺的正是「记账、验收、度量、复盘」这一层——agent-board 补上它。
|
|
10
28
|
|
|
11
29
|
## 安装
|
|
12
30
|
|
|
@@ -85,8 +103,27 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
85
103
|
> - **重启继承**:DSH 重启后同一会话的根 id 可能漂移,此时新 id 没有对应文件——若同工作区(`ownerCwd` 严格相等)存在**唯一**「原主已不在 `agents.roots()`」的看板,则自动继承:文件重命名为新 id、文件内 `ownerSession` 改写为新 id、`console.error` 留一行 `[task-board] 继承看板 <旧sid> → <新sid>`;**多个候选一律不自动接管**(记一行日志后按空板处理,防误合并,旧板仍可在 `list-boards` 全局视图里看到)。
|
|
86
104
|
> - **写盘保护**:落盘走 临时文件+rename 原子写(EPERM/EBUSY 退避重试);瞬时读失败/坏文件隔离后返回的空板**禁止回写**(防一个"读不到"的瞬间把看板覆成空板),坏文件隔离为 `.corrupt-<时间戳>` 留档不丢数据。
|
|
87
105
|
|
|
106
|
+
## 数据安全
|
|
107
|
+
|
|
108
|
+
看板数据(`~/.dsh/tasks-<sessionId>.json`)是本插件唯一的持久化状态,按三层防线保证数据不丢、事故可见:
|
|
109
|
+
|
|
110
|
+
- **原子写盘(防撕裂写)**:落盘一律「写临时文件 → `fsync` → `rename` 覆盖目标」——`rename` 在同分区是原子的,进程在写盘中途被强杀时磁盘上最多残留一个 `.tmp` 残件,看板本体永远不会是截断的半个 JSON。Windows 上 `rename` 覆盖已存在目标原生支持(实测直接覆盖成功),仅当目标被并发读句柄/Defender/索引器瞬时占用(`EPERM`/`EBUSY`/`EACCES`/`ENOTEMPTY`)时做有限退避重试;重试耗尽仍失败则降级「先 `unlink` 目标再 `rename`」并留日志——绝不丢整板。
|
|
111
|
+
- **腐坏隔离可见化(不再静默)**:加载时若 JSON 解析失败(撕裂写/重启打断导致文件中部夹杂另一版本残片),坏文件改名 `.corrupt-<时间戳>` 留档隔离(防反复 poison),同时在「架构健康」区亮一条 **err 级红条**(`healthHints`)提示「看板数据文件腐坏已隔离,历史在 `.corrupt-<ts>`,可联系恢复」,并向 owner 会话推送通知——数据不丢,也不再静默从空板重启。
|
|
112
|
+
- **自动 salvage(截断抢救)**:隔离前先尝试抢救——从首字节起截到第一个完整 JSON 值(`JSON.parse` 报错 `position` 截断法,与人工恢复同款手法),成功则以抢救出的完整板为底自动写回继续(`healthHints` 红条记「已抢救保留 N 张卡,丢弃撕裂残片」),失败才回退空板。两种情况下原始文件都留档 `.corrupt-<ts>`,可随时人工恢复。
|
|
113
|
+
|
|
88
114
|
## 功能总览
|
|
89
115
|
|
|
116
|
+
**核心能力一页速览**(`★` 为 v1.7.5–1.7.7 新栈):
|
|
117
|
+
|
|
118
|
+
- **看板**:六列状态流、拖拽 / 多选批量 / 文本·优先级·标签筛选、归档留档、会话隔离
|
|
119
|
+
- **派发**:一次性 Worker / 独立 Verifier、依赖调度(DFS 环检测)、touches 文件级排他、可续跑 Worker、Team 托管
|
|
120
|
+
- **验收治理** `★`:硬性验收脚本独立复跑、驳回全量带回重派、Verifier 自测指南(userTest)、风险队列「待你过目」、验收时序防护(Verifier 在场时主窗口抢批需显式 `force:true`;有活跃 run 的卡禁止归档;Verifier 迟到结论落 `lateVerdict` 留痕+迟到驳回直投告警,不再静默吞掉)
|
|
121
|
+
- **度量** `★`:Token 消耗(总量 / 有效 / 缓存读,按模型·按任务·按日)+ 记分卡(模型表现 / 质量趋势 / 驳回聚类)
|
|
122
|
+
- **可运维性** `★`:运行时健康自检(派发冻结 / 结算停滞告警)、异常驱动审视(风险队列 / 质量异动告警 / 批次摘要)、起草 lint、数据安全(原子写盘 / 腐坏隔离 / 自动 salvage)
|
|
123
|
+
- **学习反馈**:候选教训信号 → 主窗口沉淀(零耦合,不碰任何笔记 / 记忆工具 API)
|
|
124
|
+
|
|
125
|
+
以下各节是完整规格。
|
|
126
|
+
|
|
90
127
|
### 看板 UI
|
|
91
128
|
|
|
92
129
|
- 会话标题栏「智能看板」按钮 → 顶部抽屉面板(看板 / 团队 / 仪表盘三视图)
|
|
@@ -116,6 +153,7 @@ dsh plugin --profile web remove dsh-agent-board
|
|
|
116
153
|
- **「驳回聚类」区**(记分卡卡5,`task-muxhu1zv`):质量趋势区下方新增**驳回原因 Top3**——host 对验收驳回文本做规则法关键词聚类(不上 embedding:驳回文本结构模板化;首命中归类,占比分母=驳回总数,未匹配进「其他」兜底桶;按驳回消息自身本地日落点随统计范围裁剪),每行 TopN 类目 + 占比 + 代表原文(行 title 悬浮,各截 80 字)+ **「建议沉淀为约定」结构化文本**(形如「驳回 Top1『验收脚本未实跑』占 42%——建议在派发约定/Worker prompt 补一条:…」,一键复制按钮,沉淀动作人工);挂 `usageSummary.scoreboard.rejectionClusters`,老 host 缺字段整块不渲染,无驳回显空态。同一通道的**「按表现荐模型」hint**(卡5③):某模型在中小卡 worker 桶合并后 runs≥10 且一次通过率≥90% 且有效均值较其他每个合格模型严格低 >30% → 「架构健康」区亮黄条「模型 X 在中小卡表现最优(通过率 92% · 有效均值 0.8M,较其他合格模型低 35%),建议设为默认 workerModel」;无足够数据/无对比对象不亮
|
|
117
154
|
- 「架构健康」区(架构自省 L1):`get-tasks` 顺带对**近 50 张卡**现算四信号(纯函数零存储:touches 声明热度 ≥8 次且占比 ≥40% / 带 touches 任务滞留中位数 >2 倍 / 任务**执行**时长 p90 >45min(claimedAt→resolvedAt 纯干活口径,不含排队)/ 同路径驳回 ≥2 次),命中才在仪表盘渲染提示条(⚠️/ℹ️ 两级,最多 3 条)——让运行数据主动提示"该优化架构了"(如某文件反复成为锁热点=该拆),信号只建议不裁判;**运行时健康自检**(`task-muxhrkbg`)复用同一 hint 通道——host 侧在 poolCycle 成功轮 / settle 结算 / 幽灵回收三处记内存心跳(`state.poolHealth`,不落盘):有可派卡+有空位却 >5min 无成功派发轮 → 亮「🔴 派发循环疑似冻结」,有在跑卡却 >30min 无任何结算 → 黄级「结算通道长时间无活动」,幽灵表项回收 >0 时留一条一次性 info 记录(90s 保鲜、读到即灭)——运行事故不再只靠 e2e 翻车与肉眼发现;**质量异动告警**(异常驱动审视②):`usageSummary.scoreboard.trends` 环比——近 7 天一次通过率较再前 7 天跌 >10 个百分点(两侧样本各 ≥5 防小样本误报)→ 黄条「质量异动:一次通过率本周 X%(上周 Y%),建议抽查近期验收」,超时率 / 驳回率突变各加一条同构规则(阈值常量置顶,恰好 10pp 不亮、10.1pp 亮)
|
|
118
155
|
- 报告导出新增「## 批次摘要(近 24 小时)」段(异常驱动审视③):`buildReport` 顶部一页纸——完成 N 张 / 驳回 M 次 / Token 总量(约 X,附有效 Y)/ 异常事件计数(超时 + 幽灵回收,读现有字段与 healthHints)/ 一句话质量趋势;纯聚合现有字段、不新建状态,无数据时优雅空态(一句话「近 24 小时无活动记录」)
|
|
156
|
+
- **风险队列「待你过目」+ 审视摘要区**(异常驱动审视①,`task-muzikj1y`):`get-tasks` 对每张卡现算 `reviewHint={score,reasons[]}`(纯函数 `core.computeReviewHints`,现算零落库)——四条硬信号恒计入:**被驳回过**(`rejectCount>0` 或 `verification.verdict=rejected` 或 messages 有 rejection 包)/ **上报过歧义**(messages 有 arbitration 裁决包)/ **touches 碰核心文件**(dispatch.mjs|rpc.mjs|core.mjs)/ **full 无 acceptance**;两条软信号仅质量异动告警激活期间计入(联动加严):**diff>300 行**(`deliverable.diff` 的 git diff --stat 解析)/ **续跑≥2 次**(`runs` 中 `resume:true` 计数)。已完成/归档的命中卡带「👁 建议过目」徽章(悬浮全部理由行),FilterBar 加 chip「待你过目」一键只看未阅命中卡;打开详情页即幂等落 `reviewedAt`(唯一新落库字段,`mark-reviewed` RPC 只写一次),已阅卡徽章消失、chip 不计;详情顶部新增「审视摘要区」(仅命中时显示)四问动线——①为什么在这(理由行)/ ②改了什么(`deliverable.summary`+diffStat,标「Worker 汇报」来源徽标)/ ③机器怎么验的(`verification` 结论+验收脚本+驳回/仲裁史,标「Verifier 实证」/「系统记录」徽标)/ ④怎么亲自确认(复用 `verification.userTest` 自测指南,缺省显示 tier=internal 提示);报告「批次摘要」段加一行「未过目 N 张」
|
|
119
157
|
- 统计区耗时同口径拆分:「平均排队 / 平均执行」双行展示(平均验收单列不变)
|
|
120
158
|
- 统计区新增「调研 ROI」对比行:resolved/archived 卡按有无调研注入分组现算卡数 / 平均执行时长 / 平均 token(双组总样本 ≥4 才渲染,无调研组明显更慢时数字 warn 色提示)
|
|
121
159
|
|
|
@@ -147,7 +185,7 @@ draft → pending → in-progress → verifying → resolved → archived
|
|
|
147
185
|
- **子任务**:父子层级 + 上下文继承 + 父任务自动流转 + 级联归档(僵尸态出清:`archive-task` 对「无活跃 run 的 in-progress」——典型如被 parentKick 推进后子任务已全部归档的史诗——直接放行,有活跃 run 的仍拒);**归档子任务仍计入史诗进度并在详情留档可见**——进度分子口径 `settled = resolved | cancelled | archived`、分母也含归档,归档一张子卡不会再让史诗进度从 `0/10` 退化成 `0/9`(进度只增不减),全归档的父卡也照常显示徽章;详情子任务清单不排归档行(灰化 + 行尾「已归档」徽章 + 沉底排序,点击仍可进子卡看留档)
|
|
148
186
|
- **死会话零打扰**:15s 派发心跳对无活 root 的会话板直接跳过(不读盘/不写盘/零日志,会话重开后自动恢复派发);`create-task` 拒绝不完整 sessionId(防裸短 id 建出幻影板)
|
|
149
187
|
- **面板轮询渲染短路**:`get-tasks` 响应附 `tasksHash`,任务列表无变化时前端跳过重渲染(usage/池状态等轻量字段照常刷新)
|
|
150
|
-
- **看板抽屉稳定性(isRoot
|
|
188
|
+
- **看板抽屉稳定性(isRoot 持久语义 + 蝶变防抖)**:`isRoot` 判定改读**持久 parentSession**(根会话=无父)——休眠根会话(有板有卡但无活 agent)也算 root,入口不再分钟级消失;子代理会话(parentSession 指向父)恒不显示入口。客户端防抖仍保留兜底:已确认 root 的会话需**连续 3 次(≈9s)false** 才收起抽屉(会话生命周期内「曾确认 true」粘滞),看板不再在生成状态切换时突然消失;从未确认 root 的子代理会话仍即时隐藏入口,保护语义不变
|
|
151
189
|
- 卡片识别层:父卡显示「📦 史诗 · settled/total」徽章 + 3px 迷你进度条(`settled = resolved | cancelled | archived`,total 同口径含归档;有在跑子任务时附「▸ 在跑:标题」行);子卡标题下显示「↳ 父任务标题」;依赖未满足的待办卡底部灰字「⛓ 等待「依赖标题」」(childStats 由 host 现算,缺字段一律不渲染)
|
|
152
190
|
- 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,**含已归档留档**——归档行灰化 + 行尾「已归档」徽章并沉底,点击直达子卡详情,标题行汇总 settled/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
|
|
153
191
|
- **可选 hooks(史诗可选点位:前置/后置各是一次真实 agent 运行)**:`epic.hooks = { pre: { enabled, prompt, state, runId }, post: 同构 }`——**前置**让子任务具备开跑条件(串行闸门:未完成前该史诗的子任务一张都不派),**后置**把这批活收口(全部子任务了结后跑一遍,完成后史诗才转验证中);prompt 只给薄框架契约,具体动作由 hook agent **自行决策**,**吃不准就走歧义上报**(失败即转阻塞等人裁决:重试/跳过/放弃,不自动重跑)。hook run 记在 `epic.runs[]`(role `hook-pre`/`hook-post`),卡片带相位徽章(`⏳ 前置准备中` / `🧪 收尾中` / `⚠️ hook 失败待裁决`,无 hooks 的老卡零渲染),详情页 hooks 区可编辑 enabled+prompt、查看 state/耗时并跳转 run 会话、失败一键跳裁决区;**hooks 仅主窗口可设**(工具与 RPC 双通道门禁),commit/push 不入任何默认形态
|
|
@@ -217,11 +255,11 @@ Team 托管档独有(调度员体验):
|
|
|
217
255
|
> 兼容:旧的 `set-board-mode` / `set-team-mode` 两个 RPC 原样保留(旧客户端与脚本不受影响),
|
|
218
256
|
> 内部仍以 `boardMode` + `teamMode` 两个字段落盘,老看板文件无损;`get-tasks` 额外返回派生字段 `workMode` 供 UI 单点读取。
|
|
219
257
|
|
|
220
|
-
##
|
|
258
|
+
## 14 个 Agent 工具
|
|
221
259
|
|
|
222
260
|
| 类别 | 工具 |
|
|
223
261
|
|---|---|
|
|
224
|
-
| 任务管理 | `task_create` / `task_list` / `task_context` / `task_update` / `task_claim` / `task_resolve` / `task_verify` / `task_archive` |
|
|
262
|
+
| 任务管理 | `task_create` / `task_list` / `task_context` / `task_update` / `task_claim` / `task_resolve` / `task_verify` / `task_archive` / `task_cancel` |
|
|
225
263
|
| 池治理 | `task_terminate` / `task_intervene` / `task_arbitrate` |
|
|
226
264
|
| 子代理上报 | `board_report` / `board_verdict` |
|
|
227
265
|
|
package/index.mjs
CHANGED
|
@@ -63,13 +63,14 @@ export function apply(ctx) {
|
|
|
63
63
|
var storeDeps = { sessionCwd: session.sessionCwd, poolCycle: null }
|
|
64
64
|
var store = createStore(ctx, state, storeDeps)
|
|
65
65
|
var notify = createNotify(ctx, state, { rt: store.rt, rootForSession: session.rootForSession, withTimeout: session.withTimeout, isDispatched: session.isDispatched })
|
|
66
|
+
storeDeps.notifyBoardCorrupt = notify.notifyBoardCorrupt // 晚绑定收口(腐坏隔离/抢救通知;store.rt 只在 apply 完成后才触发)
|
|
66
67
|
var dispatch = createDispatch(ctx, state, {
|
|
67
68
|
rt: store.rt, wt: store.wt, mutateLocked: store.mutateLocked, kickCycle: store.kickCycle,
|
|
68
69
|
rootForSession: session.rootForSession,
|
|
69
70
|
// sessionCwd 不再注入 dispatch(预研清单瘦身后派发侧不读盘);rpc 侧仍需要(epic 预检路径存在性)
|
|
70
71
|
withTimeout: session.withTimeout, runsFor: session.runsFor, feedbackOn: session.feedbackOn, epicSplitOn: session.epicSplitOn,
|
|
71
72
|
pushSysNote: notify.pushSysNote, maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
|
|
72
|
-
notifyDispatched: notify.notifyDispatched,
|
|
73
|
+
notifyDispatched: notify.notifyDispatched, notifyLateReject: notify.notifyLateReject,
|
|
73
74
|
})
|
|
74
75
|
storeDeps.poolCycle = dispatch.poolCycle // 晚绑定收口
|
|
75
76
|
// 僵尸 epic 出清配套(反馈 n-mutma3mmwceq):archive-task 门禁的活性判定。
|
|
@@ -80,9 +81,10 @@ export function apply(ctx) {
|
|
|
80
81
|
getActorId: session.getActorId, resolveRoot: session.resolveRoot, toolSessionId: session.toolSessionId, rpcSessionId: session.rpcSessionId,
|
|
81
82
|
rootForSession: session.rootForSession, deriveWorkMode: session.deriveWorkMode, runsFor: session.runsFor,
|
|
82
83
|
rt: store.rt, mutateLocked: store.mutateLocked,
|
|
83
|
-
maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone,
|
|
84
|
+
maybeNotify: notify.maybeNotify, notifyTaskDone: notify.notifyTaskDone, notifyLateReject: notify.notifyLateReject,
|
|
84
85
|
pushSysNote: notify.pushSysNote, sessionCwd: session.sessionCwd, // 调研门禁③:epic 发布预检汇总投递 + 预研路径相对解析根
|
|
85
86
|
spawnOneShot: dispatch.spawnOneShot, accumulateRunUsage: dispatch.accumulateRunUsage, readContextPack: dispatch.readContextPack,
|
|
87
|
+
closeRunHistory: dispatch.closeRunHistory, // 手动终止 run 关账(doTerminate 状态无关清理,task-mv1pqy27)
|
|
86
88
|
// 上报通道结算入口(task-muwkhqf8):board_report/board_verdict 把任务推进到落定态时,
|
|
87
89
|
// 由 rpc 侧顺手对「该任务当前的 continuable Worker rec」补做收尾三件套
|
|
88
90
|
// (关 run 结局 / usage 落账 / 摘超时臂)——治「工具上报完成后 idle 事件没到 → run 永远 running」。
|
package/lib/client/board-list.js
CHANGED
|
@@ -58,12 +58,14 @@
|
|
|
58
58
|
useEffect(function () { function update() { setQ(state.filterQ); setFt(state.filterTag) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
59
59
|
function setQ2(v) { state.filterQ = v; notify() }
|
|
60
60
|
function togglePrio(p) { var i = state.filterPrio.indexOf(p); if (i >= 0) state.filterPrio.splice(i, 1); else state.filterPrio.push(p); notify() }
|
|
61
|
-
function
|
|
62
|
-
|
|
61
|
+
function toggleReview() { state.filterReview = !state.filterReview; notify() }
|
|
62
|
+
function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; state.filterReview = false; notify() }
|
|
63
|
+
var hasFilter = q.trim() || state.filterPrio.length > 0 || ft || state.filterReview
|
|
63
64
|
var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
|
|
64
65
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
|
|
65
66
|
React.createElement('input', { value: q, onChange: function (e) { setQ2(e.target.value) }, placeholder: '🔍 搜索标题/描述/ID…', style: { flex: '0 1 200px', fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text } }),
|
|
66
67
|
['critical', 'high', 'medium', 'low'].map(function (p) { var on = state.filterPrio.indexOf(p) >= 0; return React.createElement('button', { key: p, onClick: function () { togglePrio(p) }, style: Object.assign({}, chipBase, on ? { background: prioColor[p], color: C_INV, borderColor: prioColor[p] } : {}) }, prioLabel[p]) }),
|
|
68
|
+
React.createElement('button', { key: 'review', onClick: toggleReview, title: '只看命中风险信号且未阅的已完成/归档卡(已阅卡不计)', style: Object.assign({}, chipBase, state.filterReview ? { background: C.warn, color: C_INV, borderColor: C.warn } : {}) }, '👁 待你过目'),
|
|
67
69
|
allTags().length > 0 ? React.createElement('select', { value: ft, onChange: function (e) { state.filterTag = e.target.value; notify() }, style: { fontSize: 10, padding: '2px 4px', border: '1px solid ' + C.border, borderRadius: 4, background: C.card, color: C.text } }, React.createElement('option', { value: '' }, '🏷 全部标签'), allTags().map(function (g) { return React.createElement('option', { key: g, value: g }, g) })) : null,
|
|
68
70
|
hasFilter ? React.createElement('button', { onClick: clearAll, style: { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: 'none', background: C.nested, color: C.text2, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('x', 9), '清除') : null)
|
|
69
71
|
}
|
|
@@ -160,7 +162,7 @@
|
|
|
160
162
|
// Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
|
|
161
163
|
// focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
|
|
162
164
|
function activate() { if (state.selectMode) { if (state.selected[t.id]) delete state.selected[t.id]; else state.selected[t.id] = true; notify() } else { state.detailId = t.id; notify() } }
|
|
163
|
-
return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
|
|
165
|
+
return React.createElement('div', { role: 'button', tabIndex: 0, 'aria-label': t.title + '(' + (statusLabels[t.status] || t.status) + ')', draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onFocus: function () { setCardFocus(t.id) }, onBlur: function () { setCardFocus('') }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate() } }, onClick: activate, style: { outline: isCardFocus(t.id) ? '2px solid ' + C.brand : 'none', outlineOffset: 2, border: '1px solid ' + (sel ? C.brand : (t.escalation ? C.err : (critGlow ? C.err : C.border))), borderRadius: 6, padding: '6px 8px', marginBottom: 6, background: sel ? C.nested : C.card, borderLeft: '3px solid ' + (t.escalation ? C.err : pc), cursor: state.selectMode ? 'pointer' : 'grab', fontSize: 12, opacity: dragging ? 0.4 : (depBlock ? 0.65 : 1), transition: 'opacity .15s', animation: critGlow ? 'tskb-crit 2s ease-in-out infinite' : 'none' } }, React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, state.selectMode ? React.createElement('span', { style: { color: sel ? C.brand : C.text2, flexShrink: 0, marginTop: 1, display: 'inline-flex' } }, ic(sel ? 'square-check-big' : 'square', 12)) : null, React.createElement('div', { style: { fontWeight: 600, color: C.text, marginBottom: 2, wordBreak: 'break-word', flex: 1 } }, t.title), React.createElement('span', { style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', color: C.text2 }, title: pm.label }, ic(pm.icon, 10)), React.createElement('span', { style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + pc + ' 20%, transparent)', color: pc, flexShrink: 0, marginTop: 1 } }, prioLabel[t.priority] || '中'), reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, marginTop: 1 } }, '👁 建议过目') : null, noResearch(t) ? React.createElement('span', { title: '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes', style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + C.warn + ' 18%, transparent)', color: C.warn, fontWeight: 600, flexShrink: 0, marginTop: 1, whiteSpace: 'nowrap' } }, '⚠️ 无调研') : null, (t.usage && t.usage.total) ?React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0, marginTop: 1 }, title: '本任务累计 token:' + String(t.usage.total) + '(' + (t.usage.runs || 0) + ' 次 run)' }, '⛁ ' + fmtTokens(t.usage.total)) : null), t.parentId ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: parentT ? '父任务:' + parentT.title + ' (' + t.parentId + ')' : t.parentId }, '↳ ' + (parentT && parentT.title ? parentT.title : shortId(t.parentId))) : null, t.escalation ? React.createElement('div', { style: { fontSize: 10, color: C.err, fontWeight: 600, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 3 } }, ic('alert-triangle', 10), '待裁决 — 点击查看疑问') : null, t.stuckSince ? React.createElement('div', { style: { fontSize: 10, color: C.warn, fontWeight: 600, marginBottom: 2, animation: 'tskb-pulse 1.5s ease-in-out infinite' } }, '⏱ 疑似卡死 · ' + ago(t.stuckSince) + ' — 点击处理') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' } },
|
|
164
166
|
epic ? React.createElement('span', { title: '史诗父卡:' + csDone + '/' + cs.total + ' 个子任务已了结(完成/取消/归档)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + csDone + '/' + cs.total) : null,
|
|
165
167
|
// hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
|
|
166
168
|
epic ? hookPhaseBadge(t) : null,
|
|
@@ -190,35 +192,49 @@
|
|
|
190
192
|
function ArchiveView() {
|
|
191
193
|
var _R = React; var useState = _R.useState
|
|
192
194
|
var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
|
|
193
|
-
//
|
|
195
|
+
// 归档 tab 数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
|
|
194
196
|
// 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
|
|
195
197
|
// 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
|
|
196
|
-
// 全量 +
|
|
197
|
-
|
|
198
|
+
// 全量 + 按归档/取消时间倒序(archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt)+ 顶部注明条数。
|
|
199
|
+
// 含 cancelled(反馈 n-mv0zbqk... 取消通道):取消卡在待办列消失(cancelled 不在 COLUMNS)后,
|
|
200
|
+
// 归入本 tab 可见、可「恢复待办」——否则作废卡从 UI 上彻底蒸发、无法找回。
|
|
201
|
+
var archived = state.tasks.filter(function (t) { return t.status === 'archived' || t.status === 'cancelled' })
|
|
202
|
+
var cancelledCount = archived.filter(function (t) { return t.status === 'cancelled' }).length // 文案口径修正:仪表盘「已归档」只数 archived,本 tab 含 cancelled,须单列
|
|
198
203
|
var list = archived.filter(function (t) {
|
|
199
204
|
if (!q) return true
|
|
200
205
|
var qq = q.toLowerCase()
|
|
201
206
|
return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
|
|
202
207
|
}).sort(function (a, b) {
|
|
203
|
-
//
|
|
204
|
-
function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
|
|
208
|
+
// 归档/取消时间倒序:archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt(老卡可能缺 archivedAt)
|
|
209
|
+
function archTs(t) { return t.archivedAt || t.cancelledAt || t.resolvedAt || t.createdAt || '' }
|
|
205
210
|
return (archTs(b) || '').localeCompare(archTs(a) || '')
|
|
206
211
|
})
|
|
207
|
-
|
|
212
|
+
// 工作模式(与 fetchTasks 同口径):list=清单模式(恢复不重派)不弹;auto/team 恢复即重派 → confirm 门禁
|
|
213
|
+
var wm = state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))
|
|
214
|
+
function restore(id) {
|
|
215
|
+
if (wm !== 'list') {
|
|
216
|
+
if (!window.confirm('恢复待办后将按当前工作模式立即重新派发,消耗一轮 Worker+Verifier token。继续?')) return
|
|
217
|
+
}
|
|
218
|
+
rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchTasks() }).catch(function () {})
|
|
219
|
+
}
|
|
208
220
|
return React.createElement('div', null,
|
|
209
221
|
React.createElement('input', { value: q, onChange: function (e) { setQ(e.target.value); state.archQ = e.target.value }, placeholder: '检索标题 / ID / 标签…', style: { width: '100%', padding: '5px 8px', fontSize: 11, border: '1px solid ' + C.border2, borderRadius: 5, background: C.card, color: C.text, marginBottom: 8 } }),
|
|
210
|
-
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + '
|
|
211
|
-
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '
|
|
222
|
+
React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 }, title: '归档 tab 含已归档 + 已取消两类;仪表盘「已归档」只数已归档,不数已取消' }, '共 ' + archived.length + ' 条' + (cancelledCount > 0 ? '(含已取消 ' + cancelledCount + ' 条)' : '')),
|
|
223
|
+
archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档/已取消任务') :
|
|
212
224
|
list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
|
|
213
225
|
list.map(function (t) {
|
|
214
|
-
|
|
226
|
+
// 归档行点击进详情(用户 2026-10-09 指令):复用 state.detailId 机制,与看板卡同路径;
|
|
227
|
+
// 详情页对 archived 的操作区既有门禁不回归(恢复待办=保存并重置、删除仅 draft/pending/blocked)。
|
|
228
|
+
return React.createElement('div', { key: t.id, onClick: function () { state.detailId = t.id; notify() }, title: '点击查看详情', style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6, cursor: 'pointer' } },
|
|
215
229
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
216
230
|
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: t.title }, t.title),
|
|
231
|
+
reviewPending(t) ? React.createElement('span', { title: '👁 建议过目(命中风险信号):\n' + ((t.reviewHint && t.reviewHint.reasons) || []).join('\n'), style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 22%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '👁 建议过目') : null,
|
|
217
232
|
t.verification ? React.createElement('span', { title: '验收结论: ' + (t.verification.summary || '').slice(0, 100), style: { fontSize: 9, padding: '1px 5px', borderRadius: 3, background: t.verification.verdict === 'approved' ? C.ok : C.err, color: C_INV } }, t.verification.verdict === 'approved' ? '过' : '驳') : null,
|
|
218
|
-
React.createElement('span', { style: { fontSize: 9,
|
|
233
|
+
t.status === 'cancelled' ? React.createElement('span', { title: '已取消(可恢复待办)', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.warn + ' 20%, transparent)', color: C.warn, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 } }, '已取消') : null,
|
|
234
|
+
React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, (t.archivedAt || t.cancelledAt) ? ago(t.archivedAt || t.cancelledAt) : '')),
|
|
219
235
|
t.verification && t.verification.summary ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + t.verification.summary) : null,
|
|
220
236
|
React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
|
|
221
|
-
React.createElement('button', { onClick: function () { restore(t.id) }, title: '恢复为待办', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
|
|
237
|
+
React.createElement('button', { onClick: function (e) { e.stopPropagation(); restore(t.id) }, title: wm === 'list' ? '恢复为待办' : '恢复为待办(将按当前工作模式立即重新派发,消耗一轮 Worker+Verifier token)', style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '↩ 恢复待办'),
|
|
222
238
|
(function () {
|
|
223
239
|
var runs = historyRuns(t)
|
|
224
240
|
if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
|
|
@@ -227,7 +243,7 @@
|
|
|
227
243
|
var seq = r.role === 'verifier' ? (++vN) : (++wN)
|
|
228
244
|
var isV = r.role === 'verifier'
|
|
229
245
|
return React.createElement('button', {
|
|
230
|
-
key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
246
|
+
key: i, onClick: function (e) { e.stopPropagation(); if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
|
|
231
247
|
title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
|
|
232
248
|
style: { fontSize: 9, padding: '2px 7px', border: '1px solid ' + (isV ? C.warn : C.brand), borderRadius: 3, background: 'transparent', color: isV ? C.warn : C.brand, cursor: 'pointer' }
|
|
233
249
|
}, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
|
|
@@ -248,6 +264,7 @@
|
|
|
248
264
|
var _tc = useState(''), touchesRaw = _tc[0], setTouchesRaw = _tc[1]
|
|
249
265
|
var _dp = useState({}), depSel = _dp[0], setDepSel = _dp[1]
|
|
250
266
|
var _ac = useState(''), acc = _ac[0], setAcc = _ac[1]
|
|
267
|
+
var _tg = useState(''), tagsRaw = _tg[0], setTagsRaw = _tg[1]
|
|
251
268
|
var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
|
|
252
269
|
var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
|
|
253
270
|
var _er = useState(''), err = _er[0], setErr = _er[1]
|
|
@@ -262,9 +279,10 @@
|
|
|
262
279
|
var t = title.trim()
|
|
263
280
|
if (!t) { setErr('⚠️ 请先填写标题'); return }
|
|
264
281
|
var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
282
|
+
var tags = tagsRaw.split(/[,,]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
265
283
|
var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
|
|
266
284
|
setBusy(true); setErr(''); setOkMsg(''); setWarn('')
|
|
267
|
-
rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
|
|
285
|
+
rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, tags: tags, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
|
|
268
286
|
setBusy(false)
|
|
269
287
|
if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
|
|
270
288
|
fetchTasks()
|
|
@@ -300,6 +318,7 @@
|
|
|
300
318
|
React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
|
|
301
319
|
React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
|
|
302
320
|
React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))),
|
|
321
|
+
field('标签(逗号分隔,可空)', React.createElement('input', { value: tagsRaw, onChange: function (e) { setTagsRaw(e.target.value) }, placeholder: '如 bug, epic, 待验收', style: inp }), '标签 = 用于筛选的逗号分隔关键字(自动 trim + 去空)'),
|
|
303
322
|
field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
|
|
304
323
|
field('依赖(全部完成后才派发,可空)', cands.length === 0
|
|
305
324
|
? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
|
package/lib/client/dashboard.js
CHANGED
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
function batchDigest() {
|
|
211
211
|
var now = Date.now(), since = now - 86400000
|
|
212
212
|
var all = (Array.isArray(state.tasks) ? state.tasks : []).concat(Array.isArray(state.archived) ? state.archived : [])
|
|
213
|
-
var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0
|
|
213
|
+
var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0, reviewPending = 0
|
|
214
214
|
function in24(iso) { var d = Date.parse(iso); return isFinite(d) && d >= since && d <= now }
|
|
215
215
|
function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
|
|
216
216
|
all.forEach(function (t) {
|
|
@@ -229,13 +229,15 @@
|
|
|
229
229
|
tokTot += tot; tokEff += inp + outp + cw
|
|
230
230
|
if (r.outcome === 'timeout/error' || r.outcome === 'timeout') timeouts++
|
|
231
231
|
})
|
|
232
|
+
// ===== 未过目计数(异常驱动审视①⑤):命中风险信号且未阅的已完成/归档卡 =====
|
|
233
|
+
if (reviewPending(t)) reviewPending++
|
|
232
234
|
})
|
|
233
235
|
var ghosts = 0
|
|
234
236
|
var hints = Array.isArray(state.healthHints) ? state.healthHints : []
|
|
235
237
|
hints.forEach(function (h) { var m = h && h.text ? String(h.text).match(/本轮回收 (\d+) 个幽灵活跃表项/) : null; if (m) ghosts += pos(m[1]) })
|
|
236
238
|
var trend = trendResolved > 0 ? ('一次通过率 ' + Math.round(trendFirstPass / trendResolved * 100) + '%(近 24h 完成 ' + trendResolved + ' 张中 ' + trendFirstPass + ' 张零驳回)') : '近 24h 无完成记录,暂无质量趋势'
|
|
237
239
|
var has = resolved > 0 || rejected > 0 || tokTot > 0 || timeouts > 0 || ghosts > 0 || trendResolved > 0
|
|
238
|
-
return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend }
|
|
240
|
+
return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend, reviewPending: reviewPending }
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
function buildReport() {
|
|
@@ -249,10 +251,12 @@
|
|
|
249
251
|
lines.push('- Token:约 ' + fmtTokens(dg.tokTot) + (dg.tokEff > 0 ? '(其中有效 ' + fmtTokens(dg.tokEff) + ')' : ''))
|
|
250
252
|
lines.push('- 异常事件:超时 ' + dg.timeouts + ' 次 · 幽灵回收 ' + dg.ghosts + ' 次')
|
|
251
253
|
lines.push('- 质量趋势:' + dg.trend)
|
|
254
|
+
lines.push('- 未过目:' + dg.reviewPending + ' 张')
|
|
252
255
|
lines.push('')
|
|
253
256
|
} else {
|
|
254
257
|
lines.push('## 批次摘要(近 24 小时)')
|
|
255
258
|
lines.push('近 24 小时无活动记录(无完成 / 驳回 / 消耗 / 异常事件)')
|
|
259
|
+
lines.push('- 未过目:' + dg.reviewPending + ' 张')
|
|
256
260
|
lines.push('')
|
|
257
261
|
}
|
|
258
262
|
var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
|
|
@@ -578,13 +582,14 @@
|
|
|
578
582
|
// 近 14 天柱形通用画法(① 通过率 / ③ 超时率共用):cells = [{ k, rate(null=当日无样本), tip }]
|
|
579
583
|
// 今日柱 brand 高亮 + 率值顶标;无样本日画 3px 灰基线(title 写明无样本,不假装 0%)。
|
|
580
584
|
function qtDayBars(cells, color, todayKey) {
|
|
581
|
-
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46 } },
|
|
585
|
+
return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46, minWidth: 0, overflow: 'hidden' } },
|
|
582
586
|
cells.map(function (c) {
|
|
583
587
|
var isToday = c.k === todayKey
|
|
584
588
|
var h = c.rate === null ? 3 : Math.max(4, Math.round(c.rate * 34))
|
|
589
|
+
h = Math.min(22, h) // clamp 柱高:100% 柱封顶 22px——46px 绘图容器扣掉上下率值/日期标签与 gap/描边后不越出,防单日 100% 黑柱向上盖住右列
|
|
585
590
|
var kp = c.k.split('-')
|
|
586
591
|
var dLabel = Number(kp[1]) + '/' + Number(kp[2])
|
|
587
|
-
return React.createElement('div', { key: c.k, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
592
|
+
return React.createElement('div', { key: c.k, style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
|
|
588
593
|
c.rate !== null ? React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, mpPct(c.rate)) : null,
|
|
589
594
|
React.createElement('div', { style: { width: '100%', maxWidth: 22, height: h + 'px', borderRadius: 2, background: c.rate === null ? C.nested : (isToday ? C.brand : color), border: '1px solid ' + (isToday ? C.brand : (c.rate === null ? C.border : color)) } }),
|
|
590
595
|
React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, dLabel))
|
|
@@ -629,7 +634,7 @@
|
|
|
629
634
|
})
|
|
630
635
|
var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
|
|
631
636
|
var fpToday = fpCells[fpCells.length - 1]
|
|
632
|
-
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
637
|
+
var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
633
638
|
blockHead('一次通过率(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: fpToday.rate !== null ? C.brand : C.text2, fontWeight: 600 } }, fpToday.rate !== null ? mpPct(fpToday.rate) : '—')), '一次通过率 = 当天落定「已完成」任务里零驳回(rejectCount=0)的占比;驳回 = Verifier 验收判 reject'),
|
|
634
639
|
fpHas ? qtDayBars(fpCells, C.ok, todayKey)
|
|
635
640
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
|
|
@@ -657,7 +662,7 @@
|
|
|
657
662
|
})
|
|
658
663
|
var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
|
|
659
664
|
var toToday = toCells[toCells.length - 1]
|
|
660
|
-
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
|
|
665
|
+
var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
|
|
661
666
|
blockHead('超时率走势(近 14 天)', React.createElement('span', null, '今日 ', React.createElement('span', { style: { color: toToday.rate !== null ? C.warn : C.text2, fontWeight: 600 } }, toToday.rate !== null ? mpPct(toToday.rate) : '—')), '超时率 = 超时落定(timeout/error)的 run 占当天 run 的比例'),
|
|
662
667
|
toHas ? qtDayBars(toCells, C.warn, todayKey)
|
|
663
668
|
: React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
|
|
@@ -734,7 +739,7 @@
|
|
|
734
739
|
function HealthHints() {
|
|
735
740
|
var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
|
|
736
741
|
if (!hints.length) return null
|
|
737
|
-
//
|
|
742
|
+
// 三级配色:err=🚨 红(C.err,数据安全红线);warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色)
|
|
738
743
|
var infoColor = 'var(--dsw-alias-state-business-primary)'
|
|
739
744
|
return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
|
|
740
745
|
React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
|
|
@@ -742,9 +747,10 @@
|
|
|
742
747
|
React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 5, lineHeight: 1.5 } }, '运行时自检告警,多为自动恢复的瞬时异常。术语:settle = 任务结算通道;touches = 任务声明的改动文件(文件锁);p90 = 执行时长第 90 百分位'),
|
|
743
748
|
hints.map(function (h, i) {
|
|
744
749
|
var warn = h.level === 'warn'
|
|
745
|
-
var
|
|
750
|
+
var err = h.level === 'err'
|
|
751
|
+
var col = err ? C.err : (warn ? C.warn : infoColor)
|
|
746
752
|
return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'baseline', gap: 5, fontSize: 10, lineHeight: 1.5, color: col, marginBottom: 3 } },
|
|
747
|
-
React.createElement('span', { style: { flexShrink: 0 } }, warn ? '⚠️' : 'ℹ️'),
|
|
753
|
+
React.createElement('span', { style: { flexShrink: 0 } }, err ? '🚨' : (warn ? '⚠️' : 'ℹ️')),
|
|
748
754
|
React.createElement('span', null, String(h.text)))
|
|
749
755
|
}))
|
|
750
756
|
}
|
package/lib/client/kernel.js
CHANGED
|
@@ -194,6 +194,11 @@
|
|
|
194
194
|
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
195
195
|
['rect', { width: 6, height: 6, x: 9, y: 9 }]
|
|
196
196
|
],
|
|
197
|
+
// 取消入口图标(详情页「取消」按钮;语义=作废不派发,与删除 trash-2 区分)
|
|
198
|
+
'ban': [
|
|
199
|
+
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
200
|
+
['path', { d: 'm4.9 4.9 14.2 14.2' }]
|
|
201
|
+
],
|
|
197
202
|
'plus': [
|
|
198
203
|
['path', { d: 'M5 12h14' }],
|
|
199
204
|
['path', { d: 'M12 5v14' }]
|
|
@@ -229,7 +234,7 @@
|
|
|
229
234
|
blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
|
|
230
235
|
}
|
|
231
236
|
var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
|
|
232
|
-
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
|
|
237
|
+
var state = { sessionId: null, tasks: [], boardMode: 'auto', teamMode: false, workMode: 'auto', minWorkers: 1, maxWorkers: 3, minVerifiers: 0, maxVerifiers: 2, workerModel: '', verifierModel: '', softTimeoutMin: 30, hardTimeoutMin: 120, feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true, lessonPushed: {}, isRoot: true, isRootEverTrue: false, isRootFalseN: 0, isRootStable: true, open: false, detailId: null, children: [], dragOver: null, dragTask: null, dispatchInfo: '', view: 'board', layoutLeft: 280, layoutRight: 0, poolStatus: null, escalatedIds: [], filterQ: '', filterPrio: [], filterTag: '', filterReview: false, selectMode: false, selected: {}, undoSnapshot: null, cardHover: '', archSort: 'time-desc', dateRange: { from: '', to: '' }, rfOpen: false, gboOpen: false, activity: {}, archived: [], archQ: '', globalBoards: [], createOpen: false, createFlash: '', mpRole: '', mpSize: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
|
|
233
238
|
|
|
234
239
|
// #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
|
|
235
240
|
try {
|
|
@@ -351,10 +356,14 @@
|
|
|
351
356
|
state.poolStatus = (d && d.poolStatus) || null
|
|
352
357
|
// token 消耗聚合(board 级,host 端现算):范围守卫——响应回来时范围若已变,保留旧值不覆盖
|
|
353
358
|
// (新范围的那次请求会带着新聚合回来;老 host 不返回该字段 → null,零渲染)
|
|
354
|
-
// ===== usageSummary
|
|
355
|
-
//
|
|
356
|
-
// →
|
|
357
|
-
//
|
|
359
|
+
// ===== usageSummary 变化检测 + React 订阅(task-muwq9u04 → task-mv10lvud 根修)=====
|
|
360
|
+
// 病根(task-mv10lvud 实测「点今天也没变化」):范围切换时 setRange 先换 state.dateRange(新对象
|
|
361
|
+
// → setDR 身份变化 → 第一帧渲染成功),随后 fetchTasks 响应把 state.usageSummary 换成新对象并 notify(),
|
|
362
|
+
// 但 TopPanel 的 update() 里所有 setX 值都 Object.is 全等(tasks 引用不变、布尔不变)→ React 18 全量
|
|
363
|
+
// bailout,零重渲染 → Token 区永远拿不到新聚合。notify ≠ React 重渲染,须有「新身份的订阅值」做载体。
|
|
364
|
+
// 修法:TopPanel 补 usageSummary 专属 React state(setUsage(state.usageSummary)),host 每次响应都是
|
|
365
|
+
// 新对象,身份变化天然触发渲染(订阅见 TopPanel 的 update())。
|
|
366
|
+
// 为什么本处用「上一次的 JSON 串」比对而不是对象引用:host 每次 get-tasks 都现算聚合、**必然返回新对象**
|
|
358
367
|
// (引用比较恒真)→ 3s 轮询每轮都 notify,tasksHash 的渲染节约当场作废。JSON 串只在这份聚合
|
|
359
368
|
// 真变了时才不等(KB 级体积、3s 一次,代价可忽略);赋值前先快照,赋值后比对。
|
|
360
369
|
var usageJsonPrev = JSON.stringify(state.usageSummary || null)
|
|
@@ -498,18 +507,40 @@
|
|
|
498
507
|
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
499
508
|
}
|
|
500
509
|
|
|
510
|
+
// ===== 取消通道(与 host cancel-task / task_cancel 门禁一一对应)=====
|
|
511
|
+
// 可取消状态:草稿/待办/阻塞(未进入执行)。in-progress/verifying 需先终止;resolved 引导归档。
|
|
512
|
+
// 取消=显式放弃:置 cancelled + 留档可检索、可在归档 tab「恢复待办」;与删除不同,取消不丢数据。
|
|
513
|
+
function canCancel(t) { return !!t && (t.status === 'draft' || t.status === 'pending' || t.status === 'blocked') }
|
|
514
|
+
// confirm 预警(与 note「取消后不再派发,可在归档恢复」同口径);失败时 host 门禁原因原样回显。
|
|
515
|
+
function cancelTask(id, title, onMsg) {
|
|
516
|
+
var t = getTask(id)
|
|
517
|
+
var name = title || (t && t.title) || id
|
|
518
|
+
if (!window.confirm('取消后不再派发,可在归档恢复。确认取消「' + name + '」?')) return Promise.resolve({ ok: false, cancelled: true })
|
|
519
|
+
return rpc('cancel-task', { taskId: id }).then(function (r) {
|
|
520
|
+
if (r && r.ok === false) { if (onMsg) onMsg('⚠️ ' + (r.error || '取消失败')) }
|
|
521
|
+
else if (onMsg) onMsg('🚫 已取消「' + name + '」(可在归档 tab 恢复待办)')
|
|
522
|
+
fetchTasks()
|
|
523
|
+
return r
|
|
524
|
+
}).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
|
|
525
|
+
}
|
|
526
|
+
|
|
501
527
|
function onDragStart(e, task) { e.dataTransfer.setData('text/plain', JSON.stringify({ id: task.id, status: task.status })); e.dataTransfer.effectAllowed = 'move'; state.dragTask = task.id; notify() }
|
|
502
528
|
function onDragEnd() { state.dragTask = null; state.dragOver = null; notify() }
|
|
503
529
|
function onColDragOver(e, st) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (state.dragOver !== st) { state.dragOver = st; notify() } }
|
|
504
530
|
function onColDragLeave(st) { if (state.dragOver === st) { state.dragOver = null; notify() } }
|
|
505
531
|
function onColDrop(e, st) { e.preventDefault(); try { var data = JSON.parse(e.dataTransfer.getData('text/plain')); state.dragOver = null; state.dragTask = null; transition(data.id, data.status, st); notify() } catch (_) {} }
|
|
506
532
|
|
|
507
|
-
//
|
|
533
|
+
// ===== 风险队列「待你过目」客户端单一事实源(异常驱动审视①)=====
|
|
534
|
+
// 命中=host 现算的 reviewHint.score>0;待过目=命中 + 未阅(reviewedAt 空)+ 终态(resolved/archived)。
|
|
535
|
+
// 徽章 / chip 过滤 / digest 计数三处同用这一个谓词,避免口径分叉。
|
|
536
|
+
function reviewPending(t) { return !!(t && t.reviewHint && t.reviewHint.score > 0 && !t.reviewedAt && (t.status === 'resolved' || t.status === 'archived')) }
|
|
537
|
+
// #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签 + 待过目队列
|
|
508
538
|
function passFilter(t) {
|
|
509
539
|
var q = state.filterQ.trim().toLowerCase()
|
|
510
540
|
if (q && (t.title + ' ' + (t.description || '') + ' ' + t.id).toLowerCase().indexOf(q) < 0) return false
|
|
511
541
|
if (state.filterPrio.length > 0 && state.filterPrio.indexOf(t.priority || 'medium') < 0) return false
|
|
512
542
|
if (state.filterTag && (t.tags || []).indexOf(state.filterTag) < 0) return false
|
|
543
|
+
if (state.filterReview && !reviewPending(t)) return false
|
|
513
544
|
return true
|
|
514
545
|
}
|
|
515
546
|
function allTags() { var s = {}; state.tasks.forEach(function (t) { (t.tags || []).forEach(function (g) { s[g] = 1 }) }); return Object.keys(s) }
|
|
@@ -571,15 +602,16 @@
|
|
|
571
602
|
var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
|
|
572
603
|
var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
|
|
573
604
|
var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
|
|
574
|
-
|
|
605
|
+
var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
|
|
606
|
+
useEffect(function () { function update() { setOpen(state.open); setTasksState(state.tasks); setModeState(state.boardMode); setDetailId(state.detailId); setDragOver(state.dragOver); setDispatchInfo(state.dispatchInfo); setViewState(state.view); setLayL(state.layoutLeft); setLayR(state.layoutRight); setMinW(state.minWorkers); setMaxW(state.maxWorkers); setMinV(state.minVerifiers); setMaxV(state.maxVerifiers); setWorkerModel(state.workerModel); setVerifierModel(state.verifierModel); setTeamMode(state.teamMode); setWorkModeState(state.workMode); setDR(state.dateRange); setSoftT(state.softTimeoutMin); setHardT(state.hardTimeoutMin); setFbEnabled(state.feedbackEnabled); setNdOn(state.notifyDispatch); setNnOn(state.notifyDone); setEsOn(state.epicSplit); setVugOn(state.verifyUserGuide); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash); setTasksErr(state.tasksErr); setUsage(state.usageSummary) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
|
|
575
607
|
if (!open) return null
|
|
576
608
|
if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
|
|
577
609
|
var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
|
|
578
|
-
var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag
|
|
610
|
+
var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag || state.filterReview
|
|
579
611
|
if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
|
|
580
612
|
var content
|
|
581
613
|
if (view === 'dashboard') { content = React.createElement(Dashboard) }
|
|
582
|
-
else if (view === 'archive') { content = React.createElement(ArchiveView) }
|
|
614
|
+
else if (view === 'archive') { content = detailId ? React.createElement(DetailView) : React.createElement(ArchiveView) }
|
|
583
615
|
else if (view === 'team') { content = React.createElement(TeamView) }
|
|
584
616
|
else if (detailId) { content = React.createElement(DetailView) }
|
|
585
617
|
else {
|