dsh-agent-board 1.6.0 → 1.7.0

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
@@ -232,7 +232,7 @@ function apply(ctx) {
232
232
 
233
233
  var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
234
234
  var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
235
- 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, lessonPushed: {}, isRoot: 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: '', usageSummary: null }
235
+ 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, lessonPushed: {}, isRoot: 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: '', usageSummary: null, childStats: {}, tasksErr: '' }
236
236
 
237
237
  // #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
238
238
  try {
@@ -258,11 +258,26 @@ function apply(ctx) {
258
258
  function notify() { listeners.forEach(function (fn) { try { fn(state) } catch (_) {} }) }
259
259
  function rpc(method, args) { var a = args || {}; a.sessionId = state.sessionId; return fetch('/dsh-agent-board', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: method, args: a }) }).then(function (r) { return r.json() }) }
260
260
 
261
+ // ===== 读路径错误防线(反馈 n-mut9q600tnpx)=====
262
+ // {error} 响应 / 网络 reject 一律保留旧数据(旧卡片继续显示)+ 面板头部下方非阻断错误条,
263
+ // 下次成功自动消失;绝不把 {error} 当空板渲染。console.warn 30s 去抖——3s 轮询失败每次都打会刷屏。
264
+ var readErrWarnAt = 0
265
+ function reportReadErr(msg) {
266
+ state.tasksErr = msg
267
+ var now = Date.now()
268
+ if (now - readErrWarnAt > 30000) { readErrWarnAt = now; try { console.warn('[task-board] ' + msg) } catch (_) {} }
269
+ notify()
270
+ }
271
+ function clearReadErr() { state.tasksErr = '' } // 仅 fetchTasks 成功路径调用(3s 轮询兜底,任何错误条都能随之消失)
272
+ function readErrText(e) { return String((e && e.message) || e || '网络异常') }
273
+
261
274
  function fetchTasks() {
262
275
  if (!state.sessionId) return
263
276
  var epoch = reqEpoch
264
277
  rpc('get-tasks').then(function (d) {
265
278
  if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
279
+ if (d && d.error) { reportReadErr('看板数据刷新失败:' + d.error); return } // error 分支:保留旧 tasks/设置,绝不当空板渲染
280
+ clearReadErr()
266
281
  state.tasks = (d && d.tasks) || []
267
282
  state.boardMode = (d && d.boardMode) || 'auto'
268
283
  state.teamMode = !!(d && d.teamMode)
@@ -279,6 +294,12 @@ function apply(ctx) {
279
294
  state.hardTimeoutMin = (d && d.hardTimeoutMin) || 120
280
295
  state.poolStatus = (d && d.poolStatus) || null
281
296
  state.usageSummary = (d && d.usageSummary) || null // token 消耗聚合(board 级,host 端现算)
297
+ // 架构健康提示(架构自省 L1):与 tasks 同源透传,HealthHints 组件直接读 state.healthHints,
298
+ // 不再自持 rpc('get-tasks')——消灭仪表盘打开期间的双轮询。老 host 无此字段 → 空数组零渲染
299
+ state.healthHints = (d && Array.isArray(d.healthHints)) ? d.healthHints : []
300
+ // 史诗父卡语义层:childStats 由 host 按 tasks 现算({parentId:{total,resolved,active,activeTitle}})。
301
+ // 缺省兼容——老 host 不返回该字段时置空对象,卡片/详情层遇空一律不渲染相关元素
302
+ state.childStats = (d && d.childStats) || {}
282
303
  // 学习飞轮 v1 能力检测:老 host 不返回该字段 → 视为开启(默认开);只有显式 false 才关。
283
304
  state.feedbackEnabled = !(d && d.feedbackEnabled === false)
284
305
  state.isRoot = !d || d.isRoot !== false
@@ -289,10 +310,11 @@ function apply(ctx) {
289
310
  state.escalatedIds = state.tasks.filter(function (t) { return t.escalation }).map(function (t) { return t.id })
290
311
  if (newEsc.length > 0) { state.open = true; state.view = 'board'; state.detailId = newEsc[0].id }
291
312
  notify()
292
- }).catch(function () {})
313
+ }).catch(function (e) { if (epoch !== reqEpoch) return; reportReadErr('看板数据刷新失败:' + readErrText(e)) }) // 网络 reject 同口径
293
314
  }
294
315
 
295
- function fetchChildren() { if (!state.sessionId) return; var epoch = reqEpoch; rpc('list-children').then(function (d) { if (epoch !== reqEpoch) return; state.children = (d && d.children) || []; notify() }).catch(function () {}) }
316
+ // fetchChildren 同病同药:{error} 保留旧 children + 错误条;成功路径不清条(交给 fetchTasks 成功兜底清除,避免并发响应互相抢)
317
+ function fetchChildren() { if (!state.sessionId) return; var epoch = reqEpoch; rpc('list-children').then(function (d) { if (epoch !== reqEpoch) return; if (d && d.error) { reportReadErr('子任务列表刷新失败:' + d.error); return } state.children = (d && d.children) || []; notify() }).catch(function (e) { if (epoch !== reqEpoch) return; reportReadErr('子任务列表刷新失败:' + readErrText(e)) }) }
296
318
 
297
319
  // 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
298
320
  function fetchActivity() {
@@ -414,7 +436,21 @@ function apply(ctx) {
414
436
  function depsBlocked(t) { if (!Array.isArray(t.dependsOn) || t.dependsOn.length === 0) return false; return t.dependsOn.some(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }) }
415
437
  // #19 管线档位元数据
416
438
  var pipeMeta = { full: { icon: 'flask-conical', label: '全流程(执行+验证)', short: '全流程' }, work: { icon: 'file-text', label: '免验证(只做不验)', short: '免验' }, direct: { icon: 'message-square', label: '主窗口直接处理', short: '直办' } }
417
- function durOf(t) { try { var s = t.createdAt ? new Date(t.createdAt).getTime() : 0; if (!s) return ''; var eRaw = t.resolvedAt || t.archivedAt; var e = eRaw ? new Date(eRaw).getTime() : Date.now(); var m = Math.max(0, Math.round((e - s) / 60000)); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h' + (m % 60 ? (m % 60) + 'm' : '') } catch (_) { return '' } }
439
+ // ===== 耗时口径三分离(task-mutdnitw):排队 ≠ 执行 ≠ 验收,不再把排队算进耗时 =====
440
+ // waitOf:⏳ 排队时长 = createdAt → claimedAt(未领取则统计到 now)——"等了多久"
441
+ // execOf:⏱ 执行时长 = claimedAt → resolvedAt/archivedAt(未定则统计到 now)——"干了多久"
442
+ // 无 claimedAt(手工/direct 卡没有领取动作)回退原口径 createdAt → resolvedAt/archivedAt/now
443
+ // (验收时长 = resolvedAt → verifiedAt,仪表盘 verifyTimes 已有此口径,卡片不展示)
444
+ function fmtDur(s, e) { var m = Math.max(0, Math.round((e - s) / 60000)); if (m < 1) return '<1m'; if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h' + (m % 60 ? (m % 60) + 'm' : '') }
445
+ function waitOf(t) { try { var s = t.createdAt ? new Date(t.createdAt).getTime() : 0; if (!s) return ''; var e = t.claimedAt ? new Date(t.claimedAt).getTime() : Date.now(); return fmtDur(s, e) } catch (_) { return '' } }
446
+ function execOf(t) { try { var sRaw = t.claimedAt || t.createdAt; var s = sRaw ? new Date(sRaw).getTime() : 0; if (!s) return ''; var eRaw = t.resolvedAt || t.archivedAt; var e = eRaw ? new Date(eRaw).getTime() : Date.now(); return fmtDur(s, e) } catch (_) { return '' } }
447
+ // 卡片耗时徽章:draft/pending(还在排队)显「⏳ 等待」;执行后(含 blocked/归档)显「⏱ 执行」;tooltip 写清口径
448
+ function cardDur(t) {
449
+ var st = t.status
450
+ if (st === 'draft' || st === 'pending') { var w = waitOf(t); return w ? { txt: '⏳ 等待 ' + w, tip: '⏳ 排队时长:创建 → 被领取(还没被领取则统计创建至今)——"等了多久",不含执行' } : null }
451
+ var x = execOf(t)
452
+ return x ? { txt: '⏱ 执行 ' + x, tip: '⏱ 执行时长:被领取 → 完成/归档(进行中则统计领取至今)——"干了多久",不含排队与验收' + (t.claimedAt ? '' : ';本卡无领取记录(手工/直办),按创建起算') } : null
453
+ }
418
454
  function elapsedMin(iso) { var s = iso ? new Date(iso).getTime() : 0; if (!s) return 0; return Math.max(0, Math.round((Date.now() - s) / 60000)) }
419
455
  // 历史会话列表:优先用 host 留档的 t.runs(含全部重试);老任务回退到 claimedBy/verifierRun
420
456
  function historyRuns(t) {
@@ -433,7 +469,7 @@ function apply(ctx) {
433
469
  function BoardButton(props) {
434
470
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
435
471
  var _a = useState(0), pendingCount = _a[0], setPendingCount = _a[1]; var _b = useState(false), isOpen = _b[0], setIsOpen = _b[1]; var _c = useState(0), escCount = _c[0], setEscCount = _c[1]; var _d2 = useState(true), isRoot = _d2[0], setIsRoot = _d2[1]
436
- useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
472
+ useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; state.childStats = {}; state.tasksErr = ''; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId])
437
473
  useEffect(function () { function update() { var n = 0, e = 0; for (var i = 0; i < state.tasks.length; i++) { if (state.tasks[i].status === 'pending') n++; if (state.tasks[i].escalation) e++ }; setPendingCount(n); setEscCount(e); setIsOpen(state.open); setIsRoot(state.isRoot) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
438
474
  if (!isRoot) return null // 子代理会话不显示看板入口
439
475
  return React.createElement('button', { onClick: function () { state.open = !state.open; notify() }, title: '智能看板' + (pendingCount > 0 ? '(' + pendingCount + ' 待办)' : '') + (escCount > 0 ? '(' + escCount + ' 待裁决)' : ''), style: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 10px', border: '1px solid ' + (escCount > 0 ? C.err : C.border), borderRadius: 6, background: isOpen ? C.nested : 'transparent', color: C.text, cursor: 'pointer', fontSize: 12 } }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center' } }, ic('clipboard-list', 14)), React.createElement('span', null, '智能看板'), escCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.err, color: C_INV, fontSize: 10, fontWeight: 700, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 1, animation: 'tskb-pulse 1s ease-in-out infinite' }, title: escCount + ' 个任务待裁决' }, ic('alert-triangle', 10), escCount) : null, pendingCount > 0 ? React.createElement('span', { style: { minWidth: 16, height: 16, padding: '0 4px', borderRadius: 8, background: C.brand, color: C_INV, fontSize: 10, fontWeight: 600, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' } }, String(pendingCount)) : null)
@@ -448,9 +484,10 @@ function apply(ctx) {
448
484
  var _q = useState(state.teamMode), teamMode = _q[0], setTeamMode = _q[1]; var _dr = useState(state.dateRange), setDR = _dr[1]
449
485
  var _wmo = useState(state.workMode), workMode = _wmo[0], setWorkModeState = _wmo[1]
450
486
  var _cro = useState(state.createOpen), createOpen = _cro[0], setCreateOpen = _cro[1]; var _crf = useState(state.createFlash), createFlash = _crf[0], setCreateFlash = _crf[1]
487
+ var _te = useState(state.tasksErr), tasksErr = _te[0], setTasksErr = _te[1] // 读路径错误条(无错不渲染)
451
488
  var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
452
489
  var _fbo = useState(state.feedbackEnabled), fbEnabled = _fbo[0], setFbEnabled = _fbo[1]
453
- 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); setCreateOpen(state.createOpen); setCreateFlash(state.createFlash) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
490
+ 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); 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) } }, [])
454
491
  if (!open) return null
455
492
  if (state.isRoot === false) return null // 子代理会话不渲染看板面板
456
493
  var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
@@ -470,7 +507,7 @@ function apply(ctx) {
470
507
  var ta = a.archivedAt || a.resolvedAt || a.createdAt || '', tb = b.archivedAt || b.resolvedAt || b.createdAt || ''
471
508
  return state.archSort === 'time-asc' ? ta.localeCompare(tb) : tb.localeCompare(ta)
472
509
  })
473
- content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
510
+ content = React.createElement('div', null, React.createElement(FilterBar, null), hasFilter && active.length === 0 && archived.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 16, color: C.text2, fontSize: 11 } }, '无匹配任务') : null, React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, state.selectMode ? '多选模式:点击卡片勾选,底部批量操作' : '提示:拖拽卡片到目标列即可流转状态'), tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: '16px 8px 12px', color: C.text2, fontSize: 12 } }, '点击右上角', React.createElement('span', { style: { color: C.brand, fontWeight: 600 } }, '「+ 新建任务」'), '创建第一张卡,或在对话里说一句 ', React.createElement('code', { style: { fontSize: 11, background: C.nested, padding: '0 4px', borderRadius: 3 } }, 'task_create')) : null, React.createElement('div', { style: { display: 'flex', gap: 8, overflowX: 'auto', paddingBottom: 4 } }, cols), React.createElement(BatchBar, null))
474
511
  }
475
512
  return React.createElement('div', { style: { position: 'fixed', top: 44, left: (layL + 8) + 'px', right: (layR + 8) + 'px', maxHeight: '60vh', zIndex: 900, background: C.bg, border: '1px solid ' + C.border, borderRadius: '0 0 10px 10px', boxShadow: '0 8px 24px rgba(0,0,0,0.12)', display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
476
513
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0, flexWrap: 'wrap', gap: 4 } },
@@ -484,7 +521,9 @@ function apply(ctx) {
484
521
  React.createElement(WorkModeSwitch, { mode: workMode }),
485
522
  React.createElement('button', { onClick: function () { fetchTasks(); fetchChildren() }, title: '刷新', style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 12, color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('refresh-cw', 13)),
486
523
  React.createElement('button', { onClick: function () { state.open = false; state.detailId = null; notify() }, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 14, color: C.text2, display: 'inline-flex', alignItems: 'center' } }, ic('x', 14)))),
487
- React.createElement('div', { style: { flex: 1, overflowY: 'auto', padding: '10px 12px' } }, tasks.length === 0 ? React.createElement('div', { style: { textAlign: 'center', padding: 24, color: C.text2, fontSize: 12 } }, '🎉 暂无任务') : content),
524
+ tasksErr ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '4px 12px', fontSize: 11, color: C.err, background: 'rgba(239,68,68,.08)', borderBottom: '1px solid ' + C.border, flexShrink: 0 }, title: '刷新失败不影响已显示数据,下次轮询成功自动恢复' }, ic('alert-triangle', 12), React.createElement('span', null, tasksErr)) : null,
525
+ // 空态统一(反馈 n-mut9rzoq3flu):空板不再整块替换为「🎉 暂无任务」单行,统一走 content 渲染六列骨架;引导 CTA 行在 content 内部按空板条件插入
526
+ React.createElement('div', { style: { flex: 1, overflowY: 'auto', padding: '10px 12px' } }, content),
488
527
  createOpen ? React.createElement(CreateTaskForm, null) : null)
489
528
  }
490
529
 
@@ -560,14 +599,62 @@ function apply(ctx) {
560
599
  function isCardHover(id, kind) { return String(state.cardHover || '') === (id + '|' + kind) }
561
600
  function setHover(id, kind) { var v = id ? (id + '|' + kind) : ''; if (state.cardHover !== v) { state.cardHover = v; notify() } }
562
601
 
563
- function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); return React.createElement('div', { draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onClick: function () { 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() } }, style: { 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] || '中'), (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.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, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 2 } }, '⛓ 被 ' + t.dependsOn.filter(function (id) { var d = getTask(id); return !d || (d.status !== 'resolved' && d.status !== 'archived') }).length + ' 个依赖阻塞') : 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' } },
602
+ // ===== 卡片「当前动作」行摘要化(client 侧提炼;host 仍下发原文,不改 host)=====
603
+ // host 的 activity 形如 `🔧 pwsh {"command": "Get-ChildItem …"}`(90 字符硬截,常断在 JSON 中段,
604
+ // 原样展示零信息量,反馈 n-mut914xana0t)或 `💬 自由文本`。这里提炼为「工具名 + 关键参数摘要」:
605
+ // pwsh → 命令体;read/write/edit → 文件 basename;browser_* → 动作名 + 目标(target/url/ref);
606
+ // 参数摘要 40 字符内、优先在空格/路径分隔符处断点;解析失败/自由文本回退为原文前 40 字符(头部截断)。
607
+ function actCut(s, n) { s = String(s || ''); if (s.length <= n) return s; var c = s.slice(0, n); var last = c.charCodeAt(c.length - 1); if (last >= 0xD800 && last <= 0xDBFF) c = c.slice(0, -1); var sp = Math.max(c.lastIndexOf(' '), c.lastIndexOf('/'), c.lastIndexOf('\\')); if (sp >= Math.floor(n / 2)) c = c.slice(0, sp); return c.replace(/[\s/\\]+$/, '') + '…' } // 不斩断代理对(emoji)
608
+ // 从不完整 JSON 文本里抢指定 key 的第一个字符串值(host 文本常被硬截,JSON.parse 多半失败,正则直达)
609
+ function actArg(rest, key) { var m = String(rest || '').match(new RegExp('"' + key + '"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)')); return m ? m[1] : '' }
610
+ function actBase(p) { p = String(p || ''); var i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\')); return i >= 0 ? p.slice(i + 1) : p }
611
+ function activitySummary(text) {
612
+ var s = String(text || '').trim()
613
+ if (!s) return ''
614
+ if (s.indexOf('💬') === 0) return actCut(s, 40) // 自由文本:保留 💬 前缀从头部截
615
+ var body = s.replace(/^🔧\s*/, '') // 去工具调用前缀(host 格式 🔧 <tool> <argsJSON>)
616
+ var m = body.match(/^([A-Za-z_][\w-]*)\s*([\s\S]*)$/)
617
+ if (!m) return actCut(s, 40)
618
+ var tool = m[1], rest = (m[2] || '').trim()
619
+ if (!rest) return actCut(s, 40)
620
+ var v = ''
621
+ if (tool === 'pwsh') v = actArg(rest, 'command')
622
+ else if (tool === 'read' || tool === 'write' || tool === 'edit') v = actBase(actArg(rest, 'file_path'))
623
+ if (v) return tool + ' ' + actCut(v, 40)
624
+ if (tool.indexOf('browser_') === 0) {
625
+ var act = actArg(rest, 'action'); var tgt = actArg(rest, 'target') || actArg(rest, 'url') || actArg(rest, 'ref')
626
+ var parts = []; if (act) parts.push(act); if (tgt) parts.push(tgt)
627
+ if (parts.length) return tool + ' ' + actCut(parts.join(' '), 40)
628
+ }
629
+ return actCut(s, 40) // 无法解析:原文前 40 字符(从头部截,不再断在 JSON 中段)
630
+ }
631
+
632
+ // 史诗/依赖/父子识别层(childStats 缺省兼容:host 未返回该字段时一律不渲染相关元素):
633
+ // 父卡 = 元信息行首位「📦 史诗 · resolved/total」徽章 + 3px 迷你进度条 + activeTitle 非空时「▸ 在跑」行;
634
+ // 子卡 = 标题下一行小字「↳ 父任务标题」(从 state.tasks 找父卡,找不到回退 shortId);
635
+ // 依赖未满足的待办卡 = 卡片底部灰字「⛓ 等待「第一个未满足依赖标题」」(多个时附「 等 N 个」)。
636
+ // ===== 调研门禁·UI:「⚠️ 无调研」徽章判定(纯 client 侧现算,host 不下发该字段)=====
637
+ // 口径:pipeline ∈ {full, work}(direct 主窗口自处理,无需调研注入)且 touches 非空(声明了要改文件)
638
+ // 且 context.files / context.notes 皆空。context 缺省兼容:老任务无 context 对象按空处理——
639
+ // 即老任务也会亮徽章,这是预期行为(立单质量可见即问责;draft/pending 等状态照常显示,与状态无关)。
640
+ function noResearch(t) {
641
+ var pl = t.pipeline || 'full'
642
+ if (pl !== 'full' && pl !== 'work') return false
643
+ if (!Array.isArray(t.touches) || t.touches.length === 0) return false
644
+ var cx = t.context || {}
645
+ var files = Array.isArray(cx.files) ? cx.files : []
646
+ var notes = typeof cx.notes === 'string' ? cx.notes.trim() : ''
647
+ return files.length === 0 && notes.length === 0
648
+ }
649
+ function Card(props) { var t = props.task; var pc = prioColor[t.priority] || prioColor.low; var dragging = state.dragTask === t.id; var preview = (t.deliverable && t.deliverable.summary) || t.resolution; var critGlow = t.priority === 'critical' && !t.escalation; var sel = !!state.selected[t.id]; var pm = pipeOf(t); var depBlock = t.status === 'pending' && depsBlocked(t); var delOk = !state.selectMode && canDelete(t); var cs = (state.childStats && state.childStats[t.id]) || null; var epic = !!(cs && cs.total > 0); var parentT = t.parentId ? getTask(t.parentId) : null; var depWaitTitle = ''; var depWaitN = 0; if (depBlock) { t.dependsOn.forEach(function (id) { var d = getTask(id); if (!d || (d.status !== 'resolved' && d.status !== 'archived')) { depWaitN++; if (!depWaitTitle) depWaitTitle = d && d.title ? d.title : id } }) } var durB = cardDur(t); return React.createElement('div', { draggable: !state.selectMode, onDragStart: function (e) { onDragStart(e, t) }, onDragEnd: onDragEnd, onMouseEnter: function () { if (delOk) setHover(t.id, 'del') }, onMouseLeave: function () { if (delOk) setHover('', '') }, onClick: function () { 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() } }, style: { 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' } },
650
+ epic ? React.createElement('span', { title: '史诗父卡:' + cs.resolved + '/' + cs.total + ' 个子任务已完成', style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: 'color-mix(in srgb, ' + C.brand + ' 14%, transparent)', color: C.brand, fontWeight: 600 } }, '📦 史诗 · ' + cs.resolved + '/' + cs.total) : null,
564
651
  t.frozen ? React.createElement('span', { title: '已冻结:不参与自动派发(详情页可「解除冻结」)', style: { color: C.brand, fontWeight: 600 } }, '❄ 冻结') : null,
565
652
  (Array.isArray(t.waitingForTouches) && t.waitingForTouches.length) ? React.createElement('span', { title: '等文件锁释放:' + t.waitingForTouches.join('、') + '(touches 冲突,详情页可 force 越权派发)', style: { color: C.warn, fontWeight: 600 } }, '🔒 等文件释放') : null,
566
653
  React.createElement('span', { title: pm.label, style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, border: '1px solid ' + C.border } }, pm.short),
567
654
  (t.retryCount || 0) + (t.rejectCount || 0) > 0 ? React.createElement('span', { title: '重试 ' + (t.retryCount || 0) + ' 次 / 驳回 ' + (t.rejectCount || 0) + ' 次', style: { color: C.warn, fontWeight: 600 } }, '⟳' + ((t.retryCount || 0) + (t.rejectCount || 0))) : null,
568
655
  t.status === 'in-progress' && t.claimedBy ? React.createElement('span', null, '⚡ ' + shortId(t.claimedBy)) : null,
569
656
  t.assignee ? React.createElement('span', null, '👤→' + shortId(t.assignee)) : null,
570
- durOf(t) ? React.createElement('span', { title: '创建至今耗时' }, '⏱ ' + durOf(t)) : null,
657
+ durB ? React.createElement('span', { title: durB.tip }, durB.txt) : null,
571
658
  // 删除入口(仅草稿/待办/阻塞,多选模式下隐藏以免误触):hover 才由透明转红,平时不抢视觉
572
659
  delOk ? React.createElement('span', {
573
660
  onClick: function (e) { e.stopPropagation(); deleteTask(t.id, t.title) }, // 阻止冒泡:不打开详情页
@@ -575,10 +662,14 @@ function apply(ctx) {
575
662
  title: '删除任务(不可恢复;执行中请先终止,已落定请用归档)',
576
663
  style: { flexShrink: 0, marginTop: 1, display: 'inline-flex', cursor: 'pointer', color: C.err, opacity: isCardHover(t.id, 'del') ? 1 : 0, transition: 'opacity .15s' }
577
664
  }, ic('trash-2', 11)) : null),
578
- (t.status === 'in-progress' || t.status === 'verifying') && state.activity[t.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: state.activity[t.id] }, '👁 ' + state.activity[t.id]) : null,
665
+ // 史诗迷你进度条(3px 高,resolved/total 比例,ok 色填充)+ 在跑子任务行(activeTitle 非空才渲染)
666
+ epic ? React.createElement('div', { style: { height: 3, borderRadius: 2, background: C.nested, marginTop: 4, overflow: 'hidden' }, title: '子任务进度 ' + cs.resolved + '/' + cs.total }, React.createElement('div', { style: { height: '100%', width: Math.max(0, Math.min(100, Math.round((cs.resolved / cs.total) * 100))) + '%', background: C.ok, borderRadius: 2, transition: 'width .3s' } })) : null,
667
+ epic && cs.activeTitle ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '在跑子任务:' + cs.activeTitle }, '▸ 在跑:' + cs.activeTitle) : null,
668
+ // 当前动作行:展示摘要(工具名+关键参数),悬停 title 仍是 host 原文
669
+ (t.status === 'in-progress' || t.status === 'verifying') && state.activity[t.id] ? React.createElement('div', { style: { fontSize: 10, color: C.brand, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: state.activity[t.id] }, '👁 ' + activitySummary(state.activity[t.id])) : null,
579
670
  // 里程碑进展(Worker 主动上报的轻量进展,kind=progress):进行中的卡片显示最新一条 + 相对时间,无则不显示
580
671
  t.status === 'in-progress' && t.lastProgress && t.lastProgress.text ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '最近进展(' + (t.lastProgress.by || '') + ' · ' + fmtTime(t.lastProgress.at) + '):' + t.lastProgress.text }, '📈 ' + t.lastProgress.text + ' · ' + ago(t.lastProgress.at)) : null,
581
- t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null) }
672
+ t.status === 'verifying' && preview ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, '📝 ' + preview) : null, depBlock ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: '依赖全部完成后才会派发' }, '⛓ 等待「' + depWaitTitle + '」' + (depWaitN > 1 ? ' 等 ' + depWaitN + ' 个' : '')) : null) }
582
673
 
583
674
  function ArchiveView() {
584
675
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
@@ -639,6 +730,8 @@ function apply(ctx) {
639
730
  var _dr = useState(wm === 'team'), asDraft = _dr[0], setAsDraft = _dr[1]
640
731
  var _bs = useState(false), busy = _bs[0], setBusy = _bs[1]
641
732
  var _er = useState(''), err = _er[0], setErr = _er[1]
733
+ var _ok = useState(''), okMsg = _ok[0], setOkMsg = _ok[1] // 成功行(仅「创建成功但响应带 warning」的不关窗路径展示)
734
+ var _wn = useState(''), warn = _wn[0], setWarn = _wn[1] // host create-task 成功路径附带的 warning 原文(可能多条合并,原样黄色展示)
642
735
  // 依赖候选:当前看板 status 为 draft/pending 的任务
643
736
  var cands = state.tasks.filter(function (t) { return t.status === 'draft' || t.status === 'pending' })
644
737
  function close() { state.createOpen = false; notify() }
@@ -649,13 +742,17 @@ function apply(ctx) {
649
742
  if (!t) { setErr('⚠️ 请先填写标题'); return }
650
743
  var touches = touchesRaw.split(/[\n,,;;]/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
651
744
  var deps = cands.filter(function (x) { return depSel[x.id] }).map(function (x) { return x.id })
652
- setBusy(true); setErr('')
745
+ setBusy(true); setErr(''); setOkMsg(''); setWarn('')
653
746
  rpc('create-task', { title: t, description: desc, priority: prio, pipeline: pipe, touches: touches, dependsOn: deps, acceptance: acc.trim(), draft: asDraft }).then(function (r) {
654
747
  setBusy(false)
655
748
  if (r && r.ok === false) { setErr('⚠️ ' + (r.error || '创建失败')); return }
749
+ fetchTasks()
750
+ // 调研门禁·软警告:host 成功路径可附 warning 字符串(如空描述提醒,可能多条合并)——不拦截创建、
751
+ // 也不打断成功提示:表单保持打开,成功行 + ⚠️ 黄色 warning 原文行同屏展示,读完自行关闭;
752
+ // 清掉标题防「再点一次创建」连击出重复卡(空标题会被校验拦住)。无 warning 维持原关窗 + 头部 flash 行为。
753
+ if (r && r.warning) { setOkMsg(asDraft ? '✅ 已存草稿' : '✅ 已创建任务'); setWarn(String(r.warning)); setTitle(''); return }
656
754
  state.createOpen = false
657
755
  flashCreated(asDraft ? '✅ 已存草稿' : '✅ 已创建任务')
658
- fetchTasks()
659
756
  notify()
660
757
  }).catch(function (e) { setBusy(false); setErr('⚠️ ' + String(e)) })
661
758
  }
@@ -664,11 +761,16 @@ function apply(ctx) {
664
761
  function field(label, node) { return React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('div', { style: lblStyle }, label), node) }
665
762
  var btnGhost = { fontSize: 11, padding: '4px 12px', border: '1px solid ' + C.border, borderRadius: 5, background: 'transparent', color: C.text2, cursor: 'pointer' }
666
763
  var btnPrimary = { fontSize: 11, padding: '4px 14px', border: 'none', borderRadius: 5, background: C.brand, color: C_INV, fontWeight: 600, cursor: busy ? 'default' : 'pointer', opacity: busy ? 0.6 : 1 }
667
- return React.createElement('div', { style: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.35)', zIndex: 20, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', padding: 16, overflowY: 'auto' }, onClick: function (e) { if (e.target === e.currentTarget) close() } },
668
- React.createElement('div', { style: { width: 440, maxWidth: '100%', background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 12px 32px rgba(0,0,0,0.28)', padding: 12 } },
669
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 } },
764
+ // 视口级 overlay(修裁切,反馈 n-mut9rzl4mxe4):fixed 全屏遮罩脱离看板抽屉(maxHeight 60vh + overflow hidden)
765
+ // 的裁剪上下文(抽屉无 transform/filter,fixed 相对视口定位不被裁);弹窗限高 80vh、字段区内部滚动、
766
+ // 底部按钮区吸底常驻——标题与「创建并派发」始终同屏,无需盲滚。
767
+ return React.createElement('div', { style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: function (e) { if (e.target === e.currentTarget) close() } },
768
+ React.createElement('div', { style: { width: 440, maxWidth: '100%', maxHeight: '80vh', display: 'flex', flexDirection: 'column', background: C.bg, border: '1px solid ' + C.border, borderRadius: 8, boxShadow: '0 12px 32px rgba(0,0,0,0.28)', overflow: 'hidden' } },
769
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 12px', borderBottom: '1px solid ' + C.border, flexShrink: 0 } },
670
770
  React.createElement('span', { style: { fontSize: 13, fontWeight: 600, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('plus', 14), '新建任务'),
671
771
  React.createElement('button', { onClick: close, title: '关闭', style: { border: 'none', background: 'transparent', cursor: 'pointer', color: C.text2, display: 'inline-flex' } }, ic('x', 14))),
772
+ // 字段区:内容超出 80vh 时在此内部滚动
773
+ React.createElement('div', { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '10px 12px 4px' } },
672
774
  field('标题 *', React.createElement('input', { value: title, onChange: function (e) { setTitle(e.target.value) }, placeholder: '一句话说清要做什么', style: inp })),
673
775
  field('描述', React.createElement('textarea', { value: desc, onChange: function (e) { setDesc(e.target.value) }, rows: 3, placeholder: '背景 / 目标 / 约束(可空)', style: Object.assign({}, inp, { resize: 'vertical', minHeight: 46 }) })),
674
776
  React.createElement('div', { style: { display: 'flex', gap: 8 } },
@@ -689,11 +791,15 @@ function apply(ctx) {
689
791
  React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: C.text, cursor: 'pointer', marginBottom: 6, flexWrap: 'wrap' } },
690
792
  React.createElement('input', { type: 'checkbox', checked: asDraft, onChange: function (e) { setAsDraft(e.target.checked) } }),
691
793
  '存为草稿',
692
- React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)')),
693
- err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
694
- React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
695
- React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
696
- React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发')))))
794
+ React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, wm === 'team' ? '(Team 托管默认草稿;取消勾选 = 立即派发)' : '(先补依赖/上下文,稍后统一发布)'))),
795
+ // 底部:错误行 + 按钮区吸底常驻(flexShrink:0,不随字段区滚动)
796
+ React.createElement('div', { style: { flexShrink: 0, padding: '8px 12px 12px', borderTop: '1px solid ' + C.border, background: C.bg } },
797
+ okMsg ? React.createElement('div', { style: { fontSize: 11, color: C.ok, marginBottom: 6 } }, okMsg) : null,
798
+ warn ? React.createElement('div', { style: { fontSize: 11, color: C.warn, marginBottom: 6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, '⚠️ ' + warn) : null,
799
+ err ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 6 } }, err) : null,
800
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
801
+ React.createElement('button', { onClick: close, style: btnGhost }, '取消'),
802
+ React.createElement('button', { onClick: submit, disabled: busy, title: asDraft ? '创建为草稿(不派发)' : '创建并立即进入派发池', style: btnPrimary }, busy ? '创建中…' : (asDraft ? '存为草稿' : '创建并派发'))))))
697
803
  }
698
804
 
699
805
  // ============================================================================
@@ -765,6 +871,32 @@ function apply(ctx) {
765
871
  msg ? React.createElement('div', { style: { fontSize: 10, color: C.err, marginTop: 3 } }, msg) : null)
766
872
  }
767
873
 
874
+ // 父子区块(与 DepsSection 并列):
875
+ // 子卡 = 父链面包屑「↳ 史诗:<父标题>」,点击回跳父卡详情;
876
+ // 父卡 = 非归档子任务清单(状态色点 + 标题 + 状态标签,点击直达子卡详情;标题行汇总 resolved/total)。
877
+ // 数据源是 state.tasks 按 parentId 现算(parentId 为老字段),不依赖 host 的 childStats,天然缺省兼容;
878
+ // 既无父也无子的普通卡整块不渲染。嵌套史诗(本身也是子卡的父卡)两段同区块上下排列。
879
+ function FamilySection(props) {
880
+ var task = props.task
881
+ var parentT = task.parentId ? getTask(task.parentId) : null
882
+ var kids = state.tasks.filter(function (x) { return x.parentId === task.id && x.status !== 'archived' })
883
+ if (!task.parentId && kids.length === 0) return null
884
+ var resolvedN = kids.filter(function (x) { return x.status === 'resolved' }).length
885
+ function jump(id) { state.detailId = id; notify() }
886
+ return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
887
+ task.parentId ? React.createElement('div', { style: { fontSize: 11, marginBottom: kids.length ? 6 : 0 } },
888
+ React.createElement('span', { style: { color: C.text2, fontWeight: 600 } }, '↳ 史诗:'),
889
+ React.createElement('span', { onClick: function () { if (parentT) jump(task.parentId) }, title: parentT ? parentT.title + ' (' + task.parentId + ')' : task.parentId, style: { color: parentT ? C.brand : C.text2, cursor: parentT ? 'pointer' : 'default', fontWeight: 600 } }, parentT && parentT.title ? parentT.title : shortId(task.parentId))) : null,
890
+ kids.length > 0 ? React.createElement('div', null,
891
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '📦 子任务 · ' + resolvedN + '/' + kids.length + ' 已完成'),
892
+ kids.map(function (x) {
893
+ return React.createElement('div', { key: x.id, onClick: function () { jump(x.id) }, title: (statusLabels[x.status] || x.status) + ' · ' + x.id, style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 10, cursor: 'pointer' } },
894
+ React.createElement('span', { style: { width: 7, height: 7, borderRadius: '50%', background: statusColors[x.status] || C.text2, flexShrink: 0 } }),
895
+ React.createElement('span', { style: { color: C.brand, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, x.title || x.id),
896
+ React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0 } }, statusLabels[x.status] || x.status))
897
+ })) : null)
898
+ }
899
+
768
900
  function DetailView() {
769
901
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
770
902
  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]
@@ -800,6 +932,8 @@ function apply(ctx) {
800
932
  return React.createElement('div', { style: { padding: '4px 2px' } },
801
933
  React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
802
934
  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),
935
+ // 最近失败(host settle 失败路径写入 task.lastError;缺字段静默不渲染,≤2 行截断,悬停看全文)
936
+ task.lastError ? React.createElement('div', { style: { fontSize: 11, color: C.err, marginBottom: 8, padding: '4px 8px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)', display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: 2, overflow: 'hidden', wordBreak: 'break-all' }, title: String(task.lastError) }, '最近失败: ' + String(task.lastError)) : null,
803
937
  task.frozen ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.brand, borderRadius: 6, background: 'color-mix(in srgb, ' + C.brand + ' 8%, transparent)', display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
804
938
  React.createElement('span', { style: { fontWeight: 600 } }, '❄ 已冻结:不参与自动派发'),
805
939
  React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, '裁决挂起' + (task.frozenAt ? '(' + ago(task.frozenAt) + ')' : '') + ',留出补 dependsOn/上下文的时间;解冻后重新入池'),
@@ -840,8 +974,25 @@ function apply(ctx) {
840
974
  React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 3 } }, '任务描述'),
841
975
  React.createElement('textarea', { value: editDesc, onChange: function (e) { setEditDesc(e.target.value) }, rows: 3, style: { width: '100%', fontSize: 12, padding: '6px 8px', marginBottom: 6, border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
842
976
  task.context && task.context.instructions ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '指引: ' + task.context.instructions) : null,
843
- task.context && Array.isArray(task.context.files) && task.context.files.length > 0 ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, display: 'flex', alignItems: 'flex-start', gap: 4 } }, ic('file-text', 11), React.createElement('span', null, '预研文件(派发时注入子代理): ' + task.context.files.join('、'))) : null,
844
- task.context && task.context.notes ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4, whiteSpace: 'pre-wrap', maxHeight: 100, overflowY: 'auto' } }, '调研笔记: ' + task.context.notes) : null,
977
+ // ===== 调研注入清单(调研门禁·详情侧,与卡片「⚠️ 无调研」徽章同口径)=====
978
+ // 一行汇总派发时注入给 Worker/Verifier 的调研上下文:files 数量 + basename 清单(悬停看完整路径)、
979
+ // notes 字数统计(悬停看原文,超 500 字截断标注);两者皆空 → 明示「无调研注入」。
980
+ // context 缺省兼容:老任务无 context 对象按空处理——同样亮出空态,这是预期(立单质量问责)。
981
+ // 行风格对齐上方「最近失败」行(border + color-mix 淡底):空态 warn 黄调,有内容走中性 nested。
982
+ (function () {
983
+ var cx = task.context || {}
984
+ var files = Array.isArray(cx.files) ? cx.files : []
985
+ var notes = typeof cx.notes === 'string' ? cx.notes : ''
986
+ var empty = files.length === 0 && notes.trim().length === 0
987
+ var parts = []
988
+ if (files.length) parts.push(files.length + ' 个文件(' + files.map(function (f) { return actBase(f) }).join('、') + ')')
989
+ if (notes.trim()) parts.push('调研笔记 ' + notes.length + ' 字')
990
+ var tip = empty ? '本任务未附调研上下文,Worker 需自行定位——建议补 contextFiles/contextNotes'
991
+ : (files.length ? '完整路径:\n' + files.join('\n') : '') + (notes.trim() ? (files.length ? '\n\n' : '') + '调研笔记原文:\n' + (notes.length > 500 ? notes.slice(0, 500) + '…(共 ' + notes.length + ' 字)' : notes) : '')
992
+ return React.createElement('div', { style: { fontSize: 11, marginBottom: 8, padding: '4px 8px', border: '1px solid ' + (empty ? C.warn : C.border), borderRadius: 6, background: empty ? 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' : C.nested, color: empty ? C.warn : C.text2, wordBreak: 'break-word' }, title: tip },
993
+ '📎 调研注入: ' + (empty ? '无调研注入——Worker 需自行定位,建议补 contextFiles/contextNotes' : parts.join(' · ')))
994
+ })(),
995
+ React.createElement(FamilySection, { task: task }),
845
996
  React.createElement(DepsSection, { task: task }),
846
997
  task.acceptance ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.ok, fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: 4 } }, ic('flask-conical', 11), '硬性验收: ' + task.acceptance) : null,
847
998
  task.resolution ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', background: C.nested, borderRadius: 4, borderLeft: '2px solid ' + C.warn, maxHeight: 120, overflowY: 'auto', whiteSpace: 'pre-wrap' } }, '📝 ' + task.resolution) : null,
@@ -900,19 +1051,21 @@ function apply(ctx) {
900
1051
  // ============================================================================
901
1052
  // dashboard —— 仪表盘域:统计(computeStats/TrendChart/StatCard/BarRow)· 范围筛选(RangeFilter)
902
1053
  // · 全局总览(GlobalBoards)· 报告(buildReport/ReportButton)· Token 消耗(TokenUsage)
1054
+ // · 架构健康(HealthHints,架构自省 L1)
903
1055
  // · 团队池(TeamView/PoolStatus)· 设置(WorkModeSwitch/PoolCfg/MinCfg/ModelCfg/PoolCfgPopover)
904
1056
  // 本文件是 apply(ctx) 函数体片段,不可独立运行;由 scripts/build-client.cjs 按序拼接生成 lib/client.js。
905
1057
  // ============================================================================
906
1058
 
907
1059
  function computeStats(tasks) {
908
- var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {}, completionTimes = [], verifyTimes = [], todayDone = 0, todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); var recentActivity = []
1060
+ var total = tasks.length, byStatus = {}, byPriority = {}, byAgent = {}, queueTimes = [], execTimes = [], verifyTimes = [], todayDone = 0, todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); var recentActivity = []
909
1061
  var dailyDone = [] // #15 近 7 天每日完成趋势
910
1062
  for (var di = 6; di >= 0; di--) { var dayStart = new Date(todayStart); dayStart.setDate(dayStart.getDate() - di); var dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1); dailyDone.push({ label: (dayStart.getMonth() + 1) + '/' + dayStart.getDate(), count: 0, start: dayStart, end: dayEnd }) }
911
- tasks.forEach(function (t) { byStatus[t.status] = (byStatus[t.status] || 0) + 1; byPriority[t.priority || 'medium'] = (byPriority[t.priority || 'medium'] || 0) + 1; if (t.claimedBy && (t.status === 'in-progress' || t.status === 'verifying')) { if (!byAgent[t.claimedBy]) byAgent[t.claimedBy] = 0; byAgent[t.claimedBy]++ } if (t.resolvedAt && t.createdAt) completionTimes.push(new Date(t.resolvedAt) - new Date(t.createdAt)); if (t.verifiedAt && t.resolvedAt) verifyTimes.push(new Date(t.verifiedAt) - new Date(t.resolvedAt)); if (t.resolvedAt) { var rd = new Date(t.resolvedAt); if (rd >= todayStart) todayDone++; for (var k = 0; k < dailyDone.length; k++) { if (rd >= dailyDone[k].start && rd < dailyDone[k].end) { dailyDone[k].count++; break } } } if (Array.isArray(t.history)) t.history.forEach(function (h) { recentActivity.push({ taskId: t.id, title: t.title, from: h.from, to: h.to, actor: h.actor, timestamp: h.timestamp, note: h.note }) }) })
1063
+ tasks.forEach(function (t) { byStatus[t.status] = (byStatus[t.status] || 0) + 1; byPriority[t.priority || 'medium'] = (byPriority[t.priority || 'medium'] || 0) + 1; if (t.claimedBy && (t.status === 'in-progress' || t.status === 'verifying')) { if (!byAgent[t.claimedBy]) byAgent[t.claimedBy] = 0; byAgent[t.claimedBy]++ } if (t.createdAt && t.claimedAt) queueTimes.push(new Date(t.claimedAt) - new Date(t.createdAt)); if (t.claimedAt && t.resolvedAt) execTimes.push(new Date(t.resolvedAt) - new Date(t.claimedAt)); if (t.verifiedAt && t.resolvedAt) verifyTimes.push(new Date(t.verifiedAt) - new Date(t.resolvedAt)); if (t.resolvedAt) { var rd = new Date(t.resolvedAt); if (rd >= todayStart) todayDone++; for (var k = 0; k < dailyDone.length; k++) { if (rd >= dailyDone[k].start && rd < dailyDone[k].end) { dailyDone[k].count++; break } } } if (Array.isArray(t.history)) t.history.forEach(function (h) { recentActivity.push({ taskId: t.id, title: t.title, from: h.from, to: h.to, actor: h.actor, timestamp: h.timestamp, note: h.note }) }) })
912
1064
  recentActivity.sort(function (a, b) { return (b.timestamp || '').localeCompare(a.timestamp || '') }); recentActivity = recentActivity.slice(0, 10)
913
1065
  function avgMs(arr) { if (arr.length === 0) return null; var s = arr.reduce(function (a, b) { return a + b }, 0); return Math.round(s / arr.length) }
914
1066
  function fmtMs(ms) { if (!ms) return '-'; var m = Math.floor(ms / 60000); if (m < 60) return m + ' 分钟'; var h = Math.floor(m / 60); if (h < 24) return h + ' 小时 ' + (m % 60) + ' 分'; return Math.floor(h / 24) + ' 天 ' + (h % 24) + ' 时' }
915
- return { total: total, byStatus: byStatus, byPriority: byPriority, byAgent: byAgent, avgCompletion: fmtMs(avgMs(completionTimes)), avgVerify: fmtMs(avgMs(verifyTimes)), todayDone: todayDone, dailyDone: dailyDone, recentActivity: recentActivity }
1067
+ // 耗时口径三分离(task-mutdnitw):avgQueue=平均排队(创建→被领取)、avgExec=平均执行(被领取→完成),均只统计领取过的卡;avgVerify 口径不动(完成→验收)
1068
+ return { total: total, byStatus: byStatus, byPriority: byPriority, byAgent: byAgent, avgQueue: fmtMs(avgMs(queueTimes)), avgExec: fmtMs(avgMs(execTimes)), avgVerify: fmtMs(avgMs(verifyTimes)), todayDone: todayDone, dailyDone: dailyDone, recentActivity: recentActivity }
916
1069
  }
917
1070
 
918
1071
  function TrendChart(props) {
@@ -932,6 +1085,9 @@ function apply(ctx) {
932
1085
 
933
1086
  function StatCard(props) { return React.createElement('div', { style: { flex: '1 1 0', minWidth: 80, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, textAlign: 'center' } }, React.createElement('div', { style: { fontSize: 20, fontWeight: 700, color: props.color || C.text } }, String(props.value)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 2 } }, props.label)) }
934
1087
 
1088
+ // 双行指标卡(耗时口径三分离:原「平均完成时间」一个展示位拆为 排队/执行 两行,不挤占其他指标位)
1089
+ function StatCardDual(props) { return React.createElement('div', { style: { flex: '1 1 0', minWidth: 80, padding: '6px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, textAlign: 'center' }, title: props.title }, React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: props.color || C.text } }, String(props.value1)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 1 } }, props.label1), React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: props.color || C.text, marginTop: 3 } }, String(props.value2)), React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 1 } }, props.label2)) }
1090
+
935
1091
  function BarRow(props) { var pct = props.total > 0 ? (props.count / props.total * 100) : 0; return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 } }, React.createElement('span', { style: { width: 40, fontSize: 10, color: C.text2, textAlign: 'right', flexShrink: 0 } }, props.label), React.createElement('div', { style: { flex: 1, height: 8, background: C.nested, borderRadius: 4, overflow: 'hidden' } }, React.createElement('div', { style: { height: '100%', width: pct + '%', background: props.color || C.brand, borderRadius: 4, transition: 'width .3s' } })), React.createElement('span', { style: { width: 24, fontSize: 10, color: C.text2, flexShrink: 0 } }, String(props.count))) }
936
1092
 
937
1093
  // ===== 仪表盘:日期范围筛选(共享给报告与全局总览)=====
@@ -1103,17 +1259,39 @@ function apply(ctx) {
1103
1259
  }))))
1104
1260
  }
1105
1261
 
1262
+ // ===== 架构健康提示区(架构自省 L1 · 数据源:state.healthHints)=====
1263
+ // healthHints 由 host lib/health.mjs 的 computeHealthHints(tasks) 每次请求现算(纯函数零存储零 IO):
1264
+ // [{ level: 'warn'|'info', text }]。kernel fetchTasks 已将其与 tasks 同源透传进 state.healthHints,
1265
+ // 本组件不再自持 rpc('get-tasks')(消灭仪表盘打开期间的 3s 双轮询),随面板 notify 重渲染同步刷新。
1266
+ // 缺省兼容:老 host 无此字段 / 非数组 / 空数组 → 返回 null,整块不渲染(零残留)。无按钮无状态。
1267
+ function HealthHints() {
1268
+ var hints = (Array.isArray(state.healthHints) ? state.healthHints : []).filter(function (h) { return h && h.text })
1269
+ if (!hints.length) return null
1270
+ // 两级配色:warn=⚠️ 琥珀(C.warn);info=ℹ️ 蓝灰(DSH 现有 business 信息色——色板无独立 info 档)
1271
+ var infoColor = 'var(--dsw-alias-state-business-primary)'
1272
+ return React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } },
1273
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 } }, ic('activity', 11), '架构健康'),
1274
+ hints.map(function (h, i) {
1275
+ var warn = h.level === 'warn'
1276
+ var col = warn ? C.warn : infoColor
1277
+ return React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'baseline', gap: 5, fontSize: 10, lineHeight: 1.5, color: col, marginBottom: 3 } },
1278
+ React.createElement('span', { style: { flexShrink: 0 } }, warn ? '⚠️' : 'ℹ️'),
1279
+ React.createElement('span', null, String(h.text)))
1280
+ }))
1281
+ }
1282
+
1106
1283
  function Dashboard() {
1107
1284
  var stats = computeStats(state.tasks); var statusOrder = ['pending', 'in-progress', 'verifying', 'resolved', 'blocked', 'archived']; var prioOrder = ['critical', 'high', 'medium', 'low']
1108
1285
  return React.createElement('div', null,
1109
1286
  React.createElement(RangeFilter),
1110
1287
  React.createElement(GlobalBoards),
1111
1288
  React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(StatCard, { label: '总任务', value: stats.total, color: C.text }), React.createElement(StatCard, { label: '待办', value: stats.byStatus['pending'] || 0, color: C.text2 }), React.createElement(StatCard, { label: '进行中', value: stats.byStatus['in-progress'] || 0, color: C.brand }), React.createElement(StatCard, { label: '验证中', value: stats.byStatus['verifying'] || 0, color: C.warn }), React.createElement(StatCard, { label: '已完成', value: stats.byStatus['resolved'] || 0, color: C.ok }), React.createElement(StatCard, { label: '已归档', value: stats.byStatus['archived'] || 0, color: C.text2 })),
1289
+ React.createElement(HealthHints),
1112
1290
  React.createElement(TokenUsage, { usage: state.usageSummary }),
1113
1291
  React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 12, flexWrap: 'wrap' } },
1114
1292
  React.createElement('div', { style: { flex: '1 1 0', minWidth: 200, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '按状态分布'), statusOrder.map(function (s) { return React.createElement(BarRow, { key: s, label: statusLabels[s] || s, count: stats.byStatus[s] || 0, total: stats.total, color: statusColors[s] || C.brand }) })),
1115
1293
  React.createElement('div', { style: { flex: '1 1 0', minWidth: 200, padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '按优先级分布'), prioOrder.map(function (p) { return React.createElement(BarRow, { key: p, label: prioLabel[p] || p, count: stats.byPriority[p] || 0, total: stats.total, color: prioColor[p] || C.brand }) }))),
1116
- React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(StatCard, { label: '平均完成时间', value: stats.avgCompletion }), React.createElement(StatCard, { label: '平均验证时间', value: stats.avgVerify }), React.createElement(StatCard, { label: '今日完成', value: stats.todayDone, color: C.ok })),
1294
+ React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' } }, React.createElement(StatCardDual, { label1: '平均排队', value1: stats.avgQueue, label2: '平均执行', value2: stats.avgExec, title: '耗时口径三分离:⏳ 排队 = 创建 → 被领取(等了多久);⏱ 执行 = 被领取 → 完成(干了多久);均不含验收时长' }), React.createElement(StatCard, { label: '平均验证时间', value: stats.avgVerify }), React.createElement(StatCard, { label: '今日完成', value: stats.todayDone, color: C.ok })),
1117
1295
  React.createElement(TrendChart, { data: stats.dailyDone }),
1118
1296
  Object.keys(stats.byAgent).length > 0 ? React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6, marginBottom: 12 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, 'Agent 负载(进行中+验证中)'), Object.keys(stats.byAgent).map(function (aid) { return React.createElement('div', { key: aid, style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 11 } }, React.createElement(ActorLink, { id: aid }), React.createElement('span', { style: { color: C.text2 } }, stats.byAgent[aid] + ' 个任务')) })) : null,
1119
1297
  React.createElement('div', { style: { padding: '8px 10px', background: C.card, border: '1px solid ' + C.border, borderRadius: 6 } }, React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 6 } }, '最近活跃'), stats.recentActivity.length === 0 ? React.createElement('div', { style: { fontSize: 10, color: C.text2 } }, '暂无记录') : stats.recentActivity.map(function (a, i) { return React.createElement('div', { key: i, style: { fontSize: 10, color: C.text2, marginBottom: 3, display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('span', { style: { color: statusColors[a.to] || C.brand, fontWeight: 600 } }, statusLabels[a.to] || a.to), React.createElement('span', { style: { color: C.text, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, a.title), React.createElement(ActorLink, { id: a.actor }), React.createElement('span', null, ago(a.timestamp))) })))
@@ -1174,8 +1352,9 @@ function apply(ctx) {
1174
1352
  var vActive = (ps.verifiers || []).filter(function (v) { return v.busy }).length
1175
1353
  var vTotal = (ps.verifiers || []).length
1176
1354
  return React.createElement('span', { style: { fontSize: 9, color: C.text2, display: 'inline-flex', gap: 4, alignItems: 'center' } },
1177
- React.createElement('span', { title: 'Worker 池', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), wActive + '/' + wTotal),
1178
- React.createElement('span', { title: 'Verifier 池', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), vActive + '/' + vTotal))
1355
+ // 池水位 title 标注(反馈 n-mut9rzoq3flu):裸数字新用户看不懂,⚡=Worker 在跑/上限,✓=Verifier 在跑/上限
1356
+ React.createElement('span', { title: 'Worker 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('zap', 10), wActive + '/' + wTotal),
1357
+ React.createElement('span', { title: 'Verifier 在跑/上限', style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, ic('check', 10), vActive + '/' + vTotal))
1179
1358
  }
1180
1359
 
1181
1360
  // #11 头部减负:池配置收纳进 ⚙️ 弹出层(含 #17 verifier 异构模型设置)