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/lib/client.js CHANGED
@@ -207,6 +207,11 @@ function apply(ctx) {
207
207
  ['circle', { cx: 12, cy: 12, r: 10 }],
208
208
  ['rect', { width: 6, height: 6, x: 9, y: 9 }]
209
209
  ],
210
+ // 取消入口图标(详情页「取消」按钮;语义=作废不派发,与删除 trash-2 区分)
211
+ 'ban': [
212
+ ['circle', { cx: 12, cy: 12, r: 10 }],
213
+ ['path', { d: 'm4.9 4.9 14.2 14.2' }]
214
+ ],
210
215
  'plus': [
211
216
  ['path', { d: 'M5 12h14' }],
212
217
  ['path', { d: 'M12 5v14' }]
@@ -242,7 +247,7 @@ function apply(ctx) {
242
247
  blocked: { title: '阻塞:卡住待处理(等依赖/等主窗口裁决)', empty: '暂无阻塞——任务卡住待裁决时会出现在这里' }
243
248
  }
244
249
  var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
245
- 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: '' }
250
+ 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: '' }
246
251
 
247
252
  // #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
248
253
  try {
@@ -364,10 +369,14 @@ function apply(ctx) {
364
369
  state.poolStatus = (d && d.poolStatus) || null
365
370
  // token 消耗聚合(board 级,host 端现算):范围守卫——响应回来时范围若已变,保留旧值不覆盖
366
371
  // (新范围的那次请求会带着新聚合回来;老 host 不返回该字段 → null,零渲染)
367
- // ===== usageSummary 变化检测(task-muwq9u04:选范围不重渲染)=====
368
- // 病根:范围切换只让 state.usageSummary 换对象,tasksHash 与四个 cfg 开关都不变 → 无 notify
369
- // → Token 区冻在旧数字上,要等下一次任意 notify(改任务/切开关)才翻新。
370
- // 为什么用「上一次的 JSON 串」比对而不是对象引用:host 每次 get-tasks 都现算聚合、**必然返回新对象**
372
+ // ===== usageSummary 变化检测 + React 订阅(task-muwq9u04 → task-mv10lvud 根修)=====
373
+ // 病根(task-mv10lvud 实测「点今天也没变化」):范围切换时 setRange 先换 state.dateRange(新对象
374
+ // → setDR 身份变化 → 第一帧渲染成功),随后 fetchTasks 响应把 state.usageSummary 换成新对象并 notify(),
375
+ // 但 TopPanel 的 update() 里所有 setX 值都 Object.is 全等(tasks 引用不变、布尔不变)→ React 18 全量
376
+ // bailout,零重渲染 → Token 区永远拿不到新聚合。notify ≠ React 重渲染,须有「新身份的订阅值」做载体。
377
+ // 修法:TopPanel 补 usageSummary 专属 React state(setUsage(state.usageSummary)),host 每次响应都是
378
+ // 新对象,身份变化天然触发渲染(订阅见 TopPanel 的 update())。
379
+ // 为什么本处用「上一次的 JSON 串」比对而不是对象引用:host 每次 get-tasks 都现算聚合、**必然返回新对象**
371
380
  // (引用比较恒真)→ 3s 轮询每轮都 notify,tasksHash 的渲染节约当场作废。JSON 串只在这份聚合
372
381
  // 真变了时才不等(KB 级体积、3s 一次,代价可忽略);赋值前先快照,赋值后比对。
373
382
  var usageJsonPrev = JSON.stringify(state.usageSummary || null)
@@ -511,18 +520,40 @@ function apply(ctx) {
511
520
  }).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
512
521
  }
513
522
 
523
+ // ===== 取消通道(与 host cancel-task / task_cancel 门禁一一对应)=====
524
+ // 可取消状态:草稿/待办/阻塞(未进入执行)。in-progress/verifying 需先终止;resolved 引导归档。
525
+ // 取消=显式放弃:置 cancelled + 留档可检索、可在归档 tab「恢复待办」;与删除不同,取消不丢数据。
526
+ function canCancel(t) { return !!t && (t.status === 'draft' || t.status === 'pending' || t.status === 'blocked') }
527
+ // confirm 预警(与 note「取消后不再派发,可在归档恢复」同口径);失败时 host 门禁原因原样回显。
528
+ function cancelTask(id, title, onMsg) {
529
+ var t = getTask(id)
530
+ var name = title || (t && t.title) || id
531
+ if (!window.confirm('取消后不再派发,可在归档恢复。确认取消「' + name + '」?')) return Promise.resolve({ ok: false, cancelled: true })
532
+ return rpc('cancel-task', { taskId: id }).then(function (r) {
533
+ if (r && r.ok === false) { if (onMsg) onMsg('⚠️ ' + (r.error || '取消失败')) }
534
+ else if (onMsg) onMsg('🚫 已取消「' + name + '」(可在归档 tab 恢复待办)')
535
+ fetchTasks()
536
+ return r
537
+ }).catch(function (e) { if (onMsg) onMsg('⚠️ ' + String(e)); return { ok: false, error: String(e) } })
538
+ }
539
+
514
540
  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() }
515
541
  function onDragEnd() { state.dragTask = null; state.dragOver = null; notify() }
516
542
  function onColDragOver(e, st) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (state.dragOver !== st) { state.dragOver = st; notify() } }
517
543
  function onColDragLeave(st) { if (state.dragOver === st) { state.dragOver = null; notify() } }
518
544
  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 (_) {} }
519
545
 
520
- // #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签
546
+ // ===== 风险队列「待你过目」客户端单一事实源(异常驱动审视①)=====
547
+ // 命中=host 现算的 reviewHint.score>0;待过目=命中 + 未阅(reviewedAt 空)+ 终态(resolved/archived)。
548
+ // 徽章 / chip 过滤 / digest 计数三处同用这一个谓词,避免口径分叉。
549
+ function reviewPending(t) { return !!(t && t.reviewHint && t.reviewHint.score > 0 && !t.reviewedAt && (t.status === 'resolved' || t.status === 'archived')) }
550
+ // #13 筛选:文本(标题/描述/ID)+ 优先级多选 + 标签 + 待过目队列
521
551
  function passFilter(t) {
522
552
  var q = state.filterQ.trim().toLowerCase()
523
553
  if (q && (t.title + ' ' + (t.description || '') + ' ' + t.id).toLowerCase().indexOf(q) < 0) return false
524
554
  if (state.filterPrio.length > 0 && state.filterPrio.indexOf(t.priority || 'medium') < 0) return false
525
555
  if (state.filterTag && (t.tags || []).indexOf(state.filterTag) < 0) return false
556
+ if (state.filterReview && !reviewPending(t)) return false
526
557
  return true
527
558
  }
528
559
  function allTags() { var s = {}; state.tasks.forEach(function (t) { (t.tags || []).forEach(function (g) { s[g] = 1 }) }); return Object.keys(s) }
@@ -584,15 +615,16 @@ function apply(ctx) {
584
615
  var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
585
616
  var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
586
617
  var _vug = useState(state.verifyUserGuide), vugOn = _vug[0], setVugOn = _vug[1] // 自测指南开关勾选态(设置区「验收」)
587
- 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) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
618
+ var _usg = useState(state.usageSummary), setUsage = _usg[1] // Token 区聚合订阅(task-mv10lvud 根修):usageSummary 换新对象即触发渲染
619
+ 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) } }, [])
588
620
  if (!open) return null
589
621
  if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
590
622
  var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
591
- var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag
623
+ var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag || state.filterReview
592
624
  if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
593
625
  var content
594
626
  if (view === 'dashboard') { content = React.createElement(Dashboard) }
595
- else if (view === 'archive') { content = React.createElement(ArchiveView) }
627
+ else if (view === 'archive') { content = detailId ? React.createElement(DetailView) : React.createElement(ArchiveView) }
596
628
  else if (view === 'team') { content = React.createElement(TeamView) }
597
629
  else if (detailId) { content = React.createElement(DetailView) }
598
630
  else {
@@ -687,12 +719,14 @@ function apply(ctx) {
687
719
  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) } }, [])
688
720
  function setQ2(v) { state.filterQ = v; notify() }
689
721
  function togglePrio(p) { var i = state.filterPrio.indexOf(p); if (i >= 0) state.filterPrio.splice(i, 1); else state.filterPrio.push(p); notify() }
690
- function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; notify() }
691
- var hasFilter = q.trim() || state.filterPrio.length > 0 || ft
722
+ function toggleReview() { state.filterReview = !state.filterReview; notify() }
723
+ function clearAll() { state.filterQ = ''; state.filterPrio = []; state.filterTag = ''; state.filterReview = false; notify() }
724
+ var hasFilter = q.trim() || state.filterPrio.length > 0 || ft || state.filterReview
692
725
  var chipBase = { fontSize: 10, padding: '2px 8px', borderRadius: 10, border: '1px solid ' + C.border, cursor: 'pointer', background: 'transparent', color: C.text2, transition: 'all .15s' }
693
726
  return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, flexWrap: 'wrap' } },
694
727
  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 } }),
695
728
  ['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]) }),
729
+ React.createElement('button', { key: 'review', onClick: toggleReview, title: '只看命中风险信号且未阅的已完成/归档卡(已阅卡不计)', style: Object.assign({}, chipBase, state.filterReview ? { background: C.warn, color: C_INV, borderColor: C.warn } : {}) }, '👁 待你过目'),
696
730
  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,
697
731
  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)
698
732
  }
@@ -789,7 +823,7 @@ function apply(ctx) {
789
823
  // Enter/Space 触发与点击相同的激活行为(多选=切换选中,否则开详情);
790
824
  // focus 态用主题色 C.brand 2px outline(不用浏览器默认蓝框,保持主题一致)
791
825
  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() } }
792
- 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' } },
826
+ 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' } },
793
827
  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,
794
828
  // hook 相位徽章:与 📦 徽章**并存**(相位是 epic 的附加过程态,不是替代进度)
795
829
  epic ? hookPhaseBadge(t) : null,
@@ -819,35 +853,49 @@ function apply(ctx) {
819
853
  function ArchiveView() {
820
854
  var _R = React; var useState = _R.useState
821
855
  var _a = useState(state.archQ || ''), q = _a[0], setQ = _a[1]
822
- // 归档数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
856
+ // 归档 tab 数据源口径对齐仪表盘(反馈 n-muyg3e0m9z7i):直接读 state.tasks(get-tasks 全量含归档),
823
857
  // 不再依赖独立 state.archived/fetchArchived——旧路径异步填库却未接入 TopPanel 的 notify 更新项,
824
858
  // 归档 tab 打开后停在「暂无归档任务」空态,而仪表盘「已归档」计数来自 state.tasks(全量),二者对不上。
825
- // 全量 + 按归档时间倒序(archivedAt 优先,回退 resolvedAt/createdAt)+ 顶部注明条数。
826
- var archived = state.tasks.filter(function (t) { return t.status === 'archived' })
859
+ // 全量 + 按归档/取消时间倒序(archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt)+ 顶部注明条数。
860
+ // 含 cancelled(反馈 n-mv0zbqk... 取消通道):取消卡在待办列消失(cancelled 不在 COLUMNS)后,
861
+ // 归入本 tab 可见、可「恢复待办」——否则作废卡从 UI 上彻底蒸发、无法找回。
862
+ var archived = state.tasks.filter(function (t) { return t.status === 'archived' || t.status === 'cancelled' })
863
+ var cancelledCount = archived.filter(function (t) { return t.status === 'cancelled' }).length // 文案口径修正:仪表盘「已归档」只数 archived,本 tab 含 cancelled,须单列
827
864
  var list = archived.filter(function (t) {
828
865
  if (!q) return true
829
866
  var qq = q.toLowerCase()
830
867
  return (t.title || '').toLowerCase().indexOf(qq) >= 0 || (t.id || '').toLowerCase().indexOf(qq) >= 0 || (t.tags || []).join(' ').toLowerCase().indexOf(qq) >= 0
831
868
  }).sort(function (a, b) {
832
- // 归档时间倒序:archivedAt 优先,回退 resolvedAt/createdAt(老卡可能缺 archivedAt)
833
- function archTs(t) { return t.archivedAt || t.resolvedAt || t.createdAt || '' }
869
+ // 归档/取消时间倒序:archivedAt 优先,回退 cancelledAt/resolvedAt/createdAt(老卡可能缺 archivedAt)
870
+ function archTs(t) { return t.archivedAt || t.cancelledAt || t.resolvedAt || t.createdAt || '' }
834
871
  return (archTs(b) || '').localeCompare(archTs(a) || '')
835
872
  })
836
- function restore(id) { rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchTasks() }).catch(function () {}) }
873
+ // 工作模式(与 fetchTasks 同口径):list=清单模式(恢复不重派)不弹;auto/team 恢复即重派 → confirm 门禁
874
+ var wm = state.workMode || (state.teamMode ? 'team' : (state.boardMode === 'auto' ? 'auto' : 'list'))
875
+ function restore(id) {
876
+ if (wm !== 'list') {
877
+ if (!window.confirm('恢复待办后将按当前工作模式立即重新派发,消耗一轮 Worker+Verifier token。继续?')) return
878
+ }
879
+ rpc('update-task', { taskId: id, resetToPending: true }).then(function () { fetchTasks() }).catch(function () {})
880
+ }
837
881
  return React.createElement('div', null,
838
882
  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 } }),
839
- React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '共 ' + archived.length + ' 条归档(全量,与仪表盘「已归档」同口径)'),
840
- archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档任务') :
883
+ React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 }, title: '归档 tab 含已归档 + 已取消两类;仪表盘「已归档」只数已归档,不数已取消' }, '共 ' + archived.length + ' 条' + (cancelledCount > 0 ? '(含已取消 ' + cancelledCount + ' 条)' : '')),
884
+ archived.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '暂无归档/已取消任务') :
841
885
  list.length === 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, padding: 12, textAlign: 'center' } }, '无匹配结果') :
842
886
  list.map(function (t) {
843
- return React.createElement('div', { key: t.id, style: { padding: '6px 8px', marginBottom: 5, background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } },
887
+ // 归档行点击进详情(用户 2026-10-09 指令):复用 state.detailId 机制,与看板卡同路径;
888
+ // 详情页对 archived 的操作区既有门禁不回归(恢复待办=保存并重置、删除仅 draft/pending/blocked)。
889
+ 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' } },
844
890
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
845
891
  React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: C.text, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: t.title }, t.title),
892
+ 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,
846
893
  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,
847
- React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, t.archivedAt ? ago(t.archivedAt) : '')),
894
+ 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,
895
+ React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, (t.archivedAt || t.cancelledAt) ? ago(t.archivedAt || t.cancelledAt) : '')),
848
896
  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,
849
897
  React.createElement('div', { style: { display: 'flex', gap: 5, marginTop: 4, flexWrap: 'wrap' } },
850
- 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' } }, '↩ 恢复待办'),
898
+ 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' } }, '↩ 恢复待办'),
851
899
  (function () {
852
900
  var runs = historyRuns(t)
853
901
  if (!runs.length) return React.createElement('span', { style: { fontSize: 9, color: C.text2, opacity: 0.6, padding: '2px 0' } }, '无历史会话')
@@ -856,7 +904,7 @@ function apply(ctx) {
856
904
  var seq = r.role === 'verifier' ? (++vN) : (++wN)
857
905
  var isV = r.role === 'verifier'
858
906
  return React.createElement('button', {
859
- key: i, onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
907
+ key: i, onClick: function (e) { e.stopPropagation(); if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(r.id) },
860
908
  title: (isV ? 'Verifier' : 'Worker') + ' 第 ' + seq + ' 次' + (r.at ? ' · ' + ago(r.at) : '') + (r.model ? ' · ' + r.model : '') + '(' + r.id + ')',
861
909
  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' }
862
910
  }, '→ ' + (isV ? 'V' : 'W') + '#' + seq)
@@ -877,6 +925,7 @@ function apply(ctx) {
877
925
  var _tc = useState(''), touchesRaw = _tc[0], setTouchesRaw = _tc[1]
878
926
  var _dp = useState({}), depSel = _dp[0], setDepSel = _dp[1]
879
927
  var _ac = useState(''), acc = _ac[0], setAcc = _ac[1]
928
+ var _tg = useState(''), tagsRaw = _tg[0], setTagsRaw = _tg[1]
880
929
  var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
881
930
  var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
882
931
  var _er = useState(''), err = _er[0], setErr = _er[1]
@@ -891,9 +940,10 @@ function apply(ctx) {
891
940
  var t = title.trim()
892
941
  if (!t) { setErr('⚠️ 请先填写标题'); return }
893
942
  var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
943
+ var tags = tagsRaw.split(/[,,]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
894
944
  var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
895
945
  setBusy(true); setErr(''); setOkMsg(''); setWarn('')
896
- rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
946
+ 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) {
897
947
  setBusy(false)
898
948
  if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
899
949
  fetchTasks()
@@ -929,6 +979,7 @@ function apply(ctx) {
929
979
  React.createElement('option', { value: 'full' }, '全流程(执行+验证)'),
930
980
  React.createElement('option', { value: 'work' }, '免验证(只做不验)'),
931
981
  React.createElement('option', { value: 'direct' }, '主窗口处理')), '管线 = 任务走哪种流程:全流程 = Worker 做 + Verifier 验;免验证 = 只做不验;主窗口处理 = 你在对话里直接办'))),
982
+ field('标签(逗号分隔,可空)', React.createElement('input', { value: tagsRaw, onChange: function (e) { setTagsRaw(e.target.value) }, placeholder: '如 bug, epic, 待验收', style: inp }), '标签 = 用于筛选的逗号分隔关键字(自动 trim + 去空)'),
932
983
  field('touches(文件/glob,逗号或换行分隔,可空)', React.createElement('input', { value: touchesRaw, onChange: function (e) { setTouchesRaw(e.target.value) }, placeholder: 'src/a.mjs, src/**', style: inp }), 'touches = 本卡要改动的文件(glob 匹配);声明后并行任务间防互踩(文件锁)'),
933
984
  field('依赖(全部完成后才派发,可空)', cands.length === 0
934
985
  ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '当前无草稿/待办任务可选')
@@ -1172,11 +1223,79 @@ function apply(ctx) {
1172
1223
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 4, lineHeight: 1.6 } }, '每个点位是一次真实 agent 运行(不是声明式命令):prompt 只给薄框架契约与上下文,动作由 hook agent 自行决策,吃不准会歧义上报;失败即转阻塞等人裁决(重试/跳过/放弃),不会自动重跑。清空 prompt 保存 = 撤掉该点位。hooks 仅主窗口可设(host 门禁,失败原因原样回显)。'))
1173
1224
  }
1174
1225
 
1226
+ // ===== 审视摘要区(异常驱动审视①④):详情顶部新块,仅 reviewHint 命中时显示 =====
1227
+ // 四问动线 + 来源徽标,素材全用现有字段纯展示拼装(reviewHint 由 host 现算、get-tasks 透出):
1228
+ // ①为什么在这 → reviewHint.reasons(host 现算中文理由行)
1229
+ // ②改了什么 → deliverable.summary + deliverable.diff(git diff --stat 原文),标「Worker 汇报」来源徽标
1230
+ // ③机器怎么验的 → verification 结论(标「Verifier 实证」)+ 验收脚本 + 驳回/仲裁史(标「系统记录」)
1231
+ // ④怎么亲自确认 → verification.userTest 自测指南复用渲染,缺省显示 tier=internal 提示
1232
+ // 已阅(reviewedAt 非空)仍显示:已阅只收敛列表徽章/chip,详情块不消失——用户要真正过目,不能打开即闪退。
1233
+ function ReviewSummary(props) {
1234
+ var task = props.task
1235
+ var rh = task.reviewHint
1236
+ if (!rh || !(rh.score > 0)) return null
1237
+ function srcBadge(text, color) { return React.createElement('span', { style: { fontSize: 9, padding: '0 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + color + ' 16%, transparent)', color: color, fontWeight: 600, flexShrink: 0 } }, text) }
1238
+ // ③ 驳回/仲裁史:history(歧义/裁决/驳回/干预/rejected)+ messages(rejection/arbitration)
1239
+ var histLines = []
1240
+ if (Array.isArray(task.history)) task.history.forEach(function (h) { if (h && h.note && /歧义|裁决|驳回|干预|rejected/i.test(h.note)) histLines.push('[' + (h.timestamp || '') + '] ' + String(h.note).slice(0, 160)) })
1241
+ if (Array.isArray(task.messages)) task.messages.forEach(function (m) { if (m && (m.kind === 'rejection' || m.kind === 'arbitration')) histLines.push('[' + (m.kind === 'rejection' ? '驳回' : '裁决') + '] ' + String(m.text || '').slice(0, 160)) })
1242
+ histLines = histLines.slice(0, 5)
1243
+ var q3rows = []
1244
+ if (task.verification) q3rows.push(React.createElement('div', { key: 'v', style: { display: 'flex', alignItems: 'flex-start', gap: 4, marginBottom: 2 } }, srcBadge('Verifier 实证', C.ok), React.createElement('span', { style: { fontSize: 11, color: C.text } }, (task.verification.verdict === 'approved' ? '验收通过' : '验收驳回') + (task.verification.summary ? ':' + task.verification.summary : ''))))
1245
+ if (task.acceptance) q3rows.push(React.createElement('div', { key: 'a', style: { display: 'flex', alignItems: 'flex-start', gap: 4, marginBottom: 2 } }, srcBadge('系统记录', C.text2), React.createElement('span', { style: { fontSize: 10, color: C.text2, fontFamily: 'monospace' } }, '验收脚本:' + task.acceptance)))
1246
+ if (histLines.length) q3rows.push(React.createElement('div', { key: 'h', style: { display: 'flex', alignItems: 'flex-start', gap: 4 } }, srcBadge('系统记录', C.text2), React.createElement('span', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', lineHeight: 1.5 } }, '驳回/仲裁史:\n' + histLines.join('\n'))))
1247
+ // ④ 自测指南:复用现有 userTest 渲染口径,缺省 tier=internal 提示
1248
+ var q4
1249
+ var ut = task.verification && task.verification.userTest
1250
+ if (ut) {
1251
+ var tierMeta = ut.tier === 'ui' ? { c: C.ok, label: 'UI 可操作' } : (ut.tier === 'metric' ? { c: C.brand, label: '看指标' } : { c: C.text2, label: '纯内部' })
1252
+ q4 = React.createElement('div', { style: { marginBottom: 6 } },
1253
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 } }, '④ 怎么亲自确认', srcBadge('自测指南 · ' + tierMeta.label, tierMeta.c)),
1254
+ ut.gist ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 2 } }, ut.gist) : null,
1255
+ ut.steps && ut.steps.length ? React.createElement('div', { style: { fontSize: 10, color: C.text } }, ut.steps.map(function (s, i) { return React.createElement('div', { key: i, style: { marginBottom: 1 } }, (i + 1) + '. ' + s) })) : null,
1256
+ ut.expect ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '预期:' + ut.expect) : null)
1257
+ } else {
1258
+ q4 = React.createElement('div', { style: { fontSize: 10, color: C.text2, lineHeight: 1.5 } }, '④ 怎么亲自确认:本卡无用户自测指南(tier=internal / 未附 userTest)——纯内部改动,验证靠测试套件')
1259
+ }
1260
+ return React.createElement('div', { id: 'tskb-review-summary', style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 7%, transparent)' } },
1261
+ React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.warn, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), '待你过目 · 命中 ' + rh.score + ' 条风险信号'),
1262
+ React.createElement('div', { style: { marginBottom: 6 } },
1263
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '① 为什么在这'),
1264
+ (rh.reasons || []).map(function (r, i) { return React.createElement('div', { key: i, style: { fontSize: 11, color: C.text, lineHeight: 1.5 } }, '· ' + r) })),
1265
+ React.createElement('div', { style: { marginBottom: 6 } },
1266
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 } }, '② 改了什么', srcBadge('Worker 汇报', C.brand)),
1267
+ (task.deliverable && task.deliverable.summary) ? React.createElement('div', { style: { fontSize: 11, color: C.text, whiteSpace: 'pre-wrap', marginBottom: 2 } }, task.deliverable.summary) : React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2 } }, '(无开发描述)'),
1268
+ (task.deliverable && task.deliverable.diff) ? React.createElement('div', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', maxHeight: 80, overflowY: 'auto', fontFamily: 'monospace' } }, task.deliverable.diff) : null),
1269
+ q3rows.length ? React.createElement('div', { style: { marginBottom: 6 } },
1270
+ React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2, marginBottom: 2 } }, '③ 机器怎么验的'),
1271
+ q3rows) : null,
1272
+ q4)
1273
+ }
1274
+
1275
+ // 标签编辑(巡检三连①):逗号分隔 input,提交走 update-task 的 tags 通道(trim + 去空)
1276
+ function TagsEditor(props) {
1277
+ var task = props.task
1278
+ var _R = React; var useState = _R.useState
1279
+ var _s = useState((task.tags || []).join(', ')), draft = _s[0], setDraft = _s[1]
1280
+ function commit() {
1281
+ var tags = draft.split(/[,,]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
1282
+ rpc('update-task', { taskId: task.id, tags: tags }).then(function () { fetchTasks() }).catch(function () {})
1283
+ }
1284
+ return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
1285
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '🏷 标签(逗号分隔)'),
1286
+ React.createElement('div', { style: { display: 'flex', gap: 4 } },
1287
+ React.createElement('input', { value: draft, onChange: function (e) { setDraft(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') commit() }, placeholder: '如 bug, epic, 待验收', style: { flex: 1, minWidth: 0, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit', boxSizing: 'border-box' } }),
1288
+ React.createElement('button', { onClick: commit, title: '保存标签(逗号分隔,自动 trim + 去空)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')))
1289
+ }
1290
+
1175
1291
  function DetailView() {
1176
1292
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
1177
1293
  var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
1178
1294
  var _e2 = useState(''), arbAnswer = _e2[0], setArbAnswer = _e2[1]; var _f2 = useState(''), interveneMsg = _f2[0], setInterveneMsg = _f2[1]; var _g2 = useState(''), actionMsg = _g2[0], setActionMsg = _g2[1]; var _h2 = useState('resume'), arbAction = _h2[0], setArbAction = _h2[1]
1179
1295
  useEffect(function () { function update() { setMode(state.boardMode) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
1296
+ // 已阅落账(异常驱动审视①③):打开命中且未阅的详情页即幂等 mark-reviewed——
1297
+ // host 幂等只写一次;本轮渲染仍按「未阅」展示审视摘要区(fetchTasks 回来前不闪退)。
1298
+ useEffect(function () { var t = getTask(state.detailId); if (t && t.reviewHint && t.reviewHint.score > 0 && !t.reviewedAt) { rpc('mark-reviewed', { taskId: t.id }).then(fetchTasks).catch(function () {}) } }, [state.detailId])
1180
1299
  if (!task) { state.detailId = null; return React.createElement('div', { style: { padding: 20, color: C.text2 } }, '任务不存在') }
1181
1300
  function doAction(fn) { fn().then(fetchTasks).catch(function () {}) }
1182
1301
  function saveEdit() { setSaving(true); rpc('update-task', { taskId: task.id, title: editTitle, description: editDesc, resetToPending: true }).then(function () { setSaving(false); fetchTasks() }).catch(function () { setSaving(false) }) }
@@ -1227,7 +1346,9 @@ function apply(ctx) {
1227
1346
  }
1228
1347
  return React.createElement('div', { style: { padding: '4px 2px' } },
1229
1348
  React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
1230
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('span', { style: { fontSize: 10, padding: '1px 6px', borderRadius: 3, background: 'color-mix(in srgb, ' + (prioColor[task.priority] || prioColor.low) + ' 20%, transparent)', color: (prioColor[task.priority] || prioColor.low) } }, prioLabel[task.priority] || '中'), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
1349
+ React.createElement(ReviewSummary, { task: task }),
1350
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 } }, React.createElement('select', { value: task.priority || 'medium', onChange: function (e) { rpc('update-task', { taskId: task.id, priority: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '优先级(四档:紧急/高/中/低)', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: (prioColor[task.priority] || prioColor.low), fontWeight: 600 } }, ['critical', 'high', 'medium', 'low'].map(function (p) { return React.createElement('option', { key: p, value: p }, prioLabel[p] + '(' + p + ')') })), React.createElement('span', { style: { fontSize: 11, padding: '1px 8px', borderRadius: 3, background: C.nested, color: C.text } }, statusLabels[task.status] || task.status), React.createElement('select', { value: task.pipeline || 'full', onChange: function (e) { rpc('update-task', { taskId: task.id, pipeline: e.target.value }).then(fetchTasks).catch(function () {}) }, title: '管线档位', style: { fontSize: 10, padding: '1px 4px', border: '1px solid ' + C.border, borderRadius: 3, background: C.card, color: C.text2 } }, React.createElement('option', { value: 'full' }, '全流程(执行+验证)'), React.createElement('option', { value: 'work' }, '免验证(只做不验)'), React.createElement('option', { value: 'direct' }, '主窗口处理')), task.pipelineAuto ? React.createElement('span', { style: { fontSize: 9, color: C.text2 }, title: '由规则自动分类,可手动覆盖' }, 'auto') : null, isManual ? React.createElement('span', { style: { fontSize: 10, color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('user', 10), '手动派发') : null),
1351
+ React.createElement(TagsEditor, { key: task.id, task: task }),
1231
1352
  // 流转到按钮组(键盘可达的状态迁移入口,替代拖拽;无合法迁移的状态(draft/resolved 等)整块不渲染)
1232
1353
  flowBtns.length > 0 ? React.createElement('div', { role: 'group', 'aria-label': '状态流转', style: { display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap', marginBottom: 8, padding: '5px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
1233
1354
  React.createElement('span', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '⇄ 流转到:'),
@@ -1325,6 +1446,14 @@ function apply(ctx) {
1325
1446
  ut.steps && ut.steps.length ? React.createElement('div', { style: { fontSize: 10, color: C.text, marginBottom: 3 } }, ut.steps.map(function (s, i) { return React.createElement('div', { key: i, style: { marginBottom: 1 } }, (i + 1) + '. ' + s) })) : null,
1326
1447
  ut.expect ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '预期:' + ut.expect) : null)
1327
1448
  })() : null) : null,
1449
+ // ===== 迟到 Verifier 结论块(验收时序三洞 ③):verifier 结论落到已落定卡后的留痕展示 =====
1450
+ // 主窗口抢批/归档早于 Verifier 结算时,其独立结论落 t.lateVerdict 而非静默丢弃——此处整块展示
1451
+ // (verdict/summary/checks + note 说明 + 时间来源),与上方正常 verification 块并列可对照。
1452
+ task.lateVerdict ? React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + (task.lateVerdict.verdict === 'approved' ? C.ok : C.err), borderRadius: 6, background: 'color-mix(in srgb, ' + (task.lateVerdict.verdict === 'approved' ? C.ok : C.err) + ' 8%, transparent)' } },
1453
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 700, color: task.lateVerdict.verdict === 'approved' ? C.ok : C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 12), '迟到 Verifier 结论 · ' + (task.lateVerdict.by || '') + ' · ' + ago(task.lateVerdict.at)),
1454
+ React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 4 } }, String(task.lateVerdict.note || '')),
1455
+ React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 4, whiteSpace: 'pre-wrap' } }, (task.lateVerdict.verdict === 'approved' ? '验收通过' : '验收驳回') + (task.lateVerdict.summary ? ':' + task.lateVerdict.summary : '')),
1456
+ task.lateVerdict.checks ? React.createElement('div', { style: { marginTop: 4 } }, React.createElement('div', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '核对项'), React.createElement('div', { style: { fontSize: 10, color: C.text2, whiteSpace: 'pre-wrap', maxHeight: 120, overflowY: 'auto' } }, task.lateVerdict.checks)) : null) : null,
1328
1457
  React.createElement(MsgThread, { messages: task.messages, taskId: task.id }),
1329
1458
  Array.isArray(task.history) && task.history.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '流转轨迹'), React.createElement('div', { style: { fontSize: 10, color: C.text2, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, task.history.map(function (h, i) { return React.createElement('div', { key: i, style: { marginBottom: 2 } }, React.createElement('span', { style: { color: C.brand } }, statusLabels[h.to] || h.to), ' · ' + ago(h.timestamp) + ' · ', React.createElement(ActorLink, { id: h.actor }), h.note ? ' · ' + h.note : '') }))) : null,
1330
1459
  React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 6 } },
@@ -1337,6 +1466,8 @@ function apply(ctx) {
1337
1466
  task.status === 'verifying' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'approved' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.ok, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('check-circle', 10), '通过') : null,
1338
1467
  task.status === 'verifying' ? React.createElement('button', { onClick: function () { var r = window.prompt('驳回原因:'); doAction(function () { return rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: r || '' }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('x-circle', 10), '驳回') : null,
1339
1468
  task.status === 'resolved' ? React.createElement('button', { onClick: function () { doAction(function () { return rpc('archive-task', { taskId: task.id }) }) }, style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.text2, color: C_INV, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('archive', 10), '归档') : null),
1469
+ // 取消(仅草稿/待办/阻塞):confirm 预警「取消后不再派发,可在归档恢复」;与删除并列但语义=作废留档(可恢复)
1470
+ canCancel(task) ? React.createElement('button', { onClick: function () { cancelTask(task.id, task.title, setActionMsg) }, title: '取消任务(取消后不再派发,可在归档恢复待办)', style: { fontSize: 10, padding: '3px 8px', border: '1px solid ' + C.warn, borderRadius: 3, background: 'transparent', color: C.warn, cursor: 'pointer', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('ban', 10), '取消') : null,
1340
1471
  // 删除(仅草稿/待办/阻塞):confirm 防误删;被 host 门禁拒绝时把原因回显到详情页 actionMsg
1341
1472
  // 容器 div 的收尾括号已挂在「归档」行;删除按钮作为**额外一行元素**排在它后面
1342
1473
  // (多一个实参给同一个 createElement,属于合法调用),因此本行自身必须配平。
@@ -1585,7 +1716,7 @@ function apply(ctx) {
1585
1716
  function batchDigest() {
1586
1717
  var now = Date.now(), since = now - 86400000
1587
1718
  var all = (Array.isArray(state.tasks) ? state.tasks : []).concat(Array.isArray(state.archived) ? state.archived : [])
1588
- var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0
1719
+ var resolved = 0, rejected = 0, tokTot = 0, tokEff = 0, timeouts = 0, trendResolved = 0, trendFirstPass = 0, reviewPending = 0
1589
1720
  function in24(iso) { var d = Date.parse(iso); return isFinite(d) && d >= since && d <= now }
1590
1721
  function pos(v) { var n = Number(v); return isFinite(n) && n > 0 ? n : 0 }
1591
1722
  all.forEach(function (t) {
@@ -1604,13 +1735,15 @@ function apply(ctx) {
1604
1735
  tokTot += tot; tokEff += inp + outp + cw
1605
1736
  if (r.outcome === 'timeout/error' || r.outcome === 'timeout') timeouts++
1606
1737
  })
1738
+ // ===== 未过目计数(异常驱动审视①⑤):命中风险信号且未阅的已完成/归档卡 =====
1739
+ if (reviewPending(t)) reviewPending++
1607
1740
  })
1608
1741
  var ghosts = 0
1609
1742
  var hints = Array.isArray(state.healthHints) ? state.healthHints : []
1610
1743
  hints.forEach(function (h) { var m = h && h.text ? String(h.text).match(/本轮回收 (\d+) 个幽灵活跃表项/) : null; if (m) ghosts += pos(m[1]) })
1611
1744
  var trend = trendResolved > 0 ? ('一次通过率 ' + Math.round(trendFirstPass / trendResolved * 100) + '%(近 24h 完成 ' + trendResolved + ' 张中 ' + trendFirstPass + ' 张零驳回)') : '近 24h 无完成记录,暂无质量趋势'
1612
1745
  var has = resolved > 0 || rejected > 0 || tokTot > 0 || timeouts > 0 || ghosts > 0 || trendResolved > 0
1613
- return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend }
1746
+ return { has: has, resolved: resolved, rejected: rejected, tokTot: tokTot, tokEff: tokEff, timeouts: timeouts, ghosts: ghosts, trend: trend, reviewPending: reviewPending }
1614
1747
  }
1615
1748
 
1616
1749
  function buildReport() {
@@ -1624,10 +1757,12 @@ function apply(ctx) {
1624
1757
  lines.push('- Token:约 ' + fmtTokens(dg.tokTot) + (dg.tokEff > 0 ? '(其中有效 ' + fmtTokens(dg.tokEff) + ')' : ''))
1625
1758
  lines.push('- 异常事件:超时 ' + dg.timeouts + ' 次 · 幽灵回收 ' + dg.ghosts + ' 次')
1626
1759
  lines.push('- 质量趋势:' + dg.trend)
1760
+ lines.push('- 未过目:' + dg.reviewPending + ' 张')
1627
1761
  lines.push('')
1628
1762
  } else {
1629
1763
  lines.push('## 批次摘要(近 24 小时)')
1630
1764
  lines.push('近 24 小时无活动记录(无完成 / 驳回 / 消耗 / 异常事件)')
1765
+ lines.push('- 未过目:' + dg.reviewPending + ' 张')
1631
1766
  lines.push('')
1632
1767
  }
1633
1768
  var groups = { inProgress: [], verifying: [], pending: [], blocked: [], resolved: [] }
@@ -1953,13 +2088,14 @@ function apply(ctx) {
1953
2088
  // 近 14 天柱形通用画法(① 通过率 / ③ 超时率共用):cells = [{ k, rate(null=当日无样本), tip }]
1954
2089
  // 今日柱 brand 高亮 + 率值顶标;无样本日画 3px 灰基线(title 写明无样本,不假装 0%)。
1955
2090
  function qtDayBars(cells, color, todayKey) {
1956
- return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46 } },
2091
+ return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: 46, minWidth: 0, overflow: 'hidden' } },
1957
2092
  cells.map(function (c) {
1958
2093
  var isToday = c.k === todayKey
1959
2094
  var h = c.rate === null ? 3 : Math.max(4, Math.round(c.rate * 34))
2095
+ h = Math.min(22, h) // clamp 柱高:100% 柱封顶 22px——46px 绘图容器扣掉上下率值/日期标签与 gap/描边后不越出,防单日 100% 黑柱向上盖住右列
1960
2096
  var kp = c.k.split('-')
1961
2097
  var dLabel = Number(kp[1]) + '/' + Number(kp[2])
1962
- return React.createElement('div', { key: c.k, style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', gap: 1 }, title: c.tip },
2098
+ 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 },
1963
2099
  c.rate !== null ? React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, mpPct(c.rate)) : null,
1964
2100
  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)) } }),
1965
2101
  React.createElement('span', { style: { fontSize: 8, color: isToday ? C.brand : C.text2 } }, dLabel))
@@ -2004,7 +2140,7 @@ function apply(ctx) {
2004
2140
  })
2005
2141
  var fpHas = false; fpCells.forEach(function (c) { if (c.rate !== null) fpHas = true })
2006
2142
  var fpToday = fpCells[fpCells.length - 1]
2007
- var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
2143
+ var block1 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
2008
2144
  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'),
2009
2145
  fpHas ? qtDayBars(fpCells, C.ok, todayKey)
2010
2146
  : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 resolved 任务落定'))
@@ -2032,7 +2168,7 @@ function apply(ctx) {
2032
2168
  })
2033
2169
  var toHas = false; toCells.forEach(function (c) { if (c.rate !== null) toHas = true })
2034
2170
  var toToday = toCells[toCells.length - 1]
2035
- var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230 } },
2171
+ var block3 = React.createElement('div', { style: { flex: '1 1 260px', minWidth: 230, overflow: 'hidden' } },
2036
2172
  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 的比例'),
2037
2173
  toHas ? qtDayBars(toCells, C.warn, todayKey)
2038
2174
  : React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '近 14 天窗口内暂无 run 落定'))
@@ -2109,7 +2245,7 @@ function apply(ctx) {
2109
2245
  function HealthHints() {
2110
2246
  var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
2111
2247
  if (!hints.length) return null
2112
- // 两级配色:warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色——色板无独立 info 档)
2248
+ // 三级配色:err=🚨 红(C.err,数据安全红线);warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色)
2113
2249
  var infoColor = 'var(--dsw-alias-state-business-primary)'
2114
2250
  return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
2115
2251
  React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
@@ -2117,9 +2253,10 @@ function apply(ctx) {
2117
2253
  React.createElement('div', { style: { fontSize: 9, color: C.text2, marginBottom: 5, lineHeight: 1.5 } }, '运行时自检告警,多为自动恢复的瞬时异常。术语:settle = 任务结算通道;touches = 任务声明的改动文件(文件锁);p90 = 执行时长第 90 百分位'),
2118
2254
  hints.map(function (h, i) {
2119
2255
  var warn = h.level === 'warn'
2120
- var col = warn ? C.warn : infoColor
2256
+ var err = h.level === 'err'
2257
+ var col = err ? C.err : (warn ? C.warn : infoColor)
2121
2258
  return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'baseline', gap: 5, fontSize: 10, lineHeight: 1.5, color: col, marginBottom: 3 } },
2122
- React.createElement('span', { style: { flexShrink: 0 } }, warn ? '⚠️' : 'ℹ️'),
2259
+ React.createElement('span', { style: { flexShrink: 0 } }, err ? '🚨' : (warn ? '⚠️' : 'ℹ️')),
2123
2260
  React.createElement('span', null, String(h.text)))
2124
2261
  }))
2125
2262
  }