dsh-agent-board 1.7.0 → 1.7.2

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.
@@ -219,7 +219,7 @@
219
219
 
220
220
  var COLUMNS = ['draft', 'pending', 'in-progress', 'verifying', 'resolved', 'blocked']
221
221
  var reqEpoch = 0 // 会话切换纪元:切会话时自增,旧会话在途响应按纪元丢弃
222
- 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: '' }
222
+ 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, 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: '', usageSummary: null, childStats: {}, tasksErr: '', tasksHash: '' }
223
223
 
224
224
  // #16 快捷键:Esc 逐级关闭(详情→看板→面板);输入框聚焦时不劫持
225
225
  try {
@@ -258,6 +258,32 @@
258
258
  function clearReadErr() { state.tasksErr = '' } // 仅 fetchTasks 成功路径调用(3s 轮询兜底,任何错误条都能随之消失)
259
259
  function readErrText(e) { return String((e && e.message) || e || '网络异常') }
260
260
 
261
+ // ===== isRoot 蝶变防抖(反馈 n-muuerxv9ijxs / task-muupr8ld)=====
262
+ // 为什么防抖:host rpc.mjs L97 的 isRoot 是每次 get-tasks 现算的
263
+ // (agents.roots() 是否含本会话 id)。生成开始/结束瞬间 agents 树重建,roots() 存在一个
264
+ // 瞬态窗口返回不含本 sid → 单次 isRoot=false 就触发 L303 强收抽屉(state.open=false),
265
+ // 用户表现为「看板在生成状态切换时突然消失,每次都要重新打开」;按钮/面板同病闪烁。
266
+ // 防抖口径:已确认过 true 的会话(everTrue)需连续 IS_ROOT_FALSE_LIMIT 次 false
267
+ // (host 3s 心跳 ≈ 9s)才把 isRootStable 翻成 false——覆盖瞬态窗口,真正降级仍会收敛。
268
+ // 为什么子代理会话不防抖:everTrue=false 的会话(新打开的子代理)立即 isRootStable=false,
269
+ // 保护语义不削弱——子代理会话本就不该出现看板入口/抽屉,即时收起没有代价。
270
+ // 消费口径:强收(本函数)/按钮(BoardButton)/面板(TopPanel)/活动心跳省流(fetchActivity)
271
+ // 四处一律读 isRootStable,不读单次轮询的 state.isRoot 原始值。
272
+ var IS_ROOT_FALSE_LIMIT = 3
273
+ function applyIsRoot(rawIsRoot) {
274
+ state.isRoot = rawIsRoot
275
+ if (rawIsRoot) {
276
+ state.isRootEverTrue = true // 曾确认 true:此后 false 需连续累计(会话生命周期内粘滞,切换会话时重置)
277
+ state.isRootFalseN = 0
278
+ state.isRootStable = true
279
+ } else if (!state.isRootEverTrue) {
280
+ state.isRootStable = false // 从未 true(新打开的子代理会话):即时收起,不防抖
281
+ } else {
282
+ state.isRootFalseN++
283
+ if (state.isRootFalseN >= IS_ROOT_FALSE_LIMIT) state.isRootStable = false // 连续 3 次 false:真降级(子代理树已重建不含本 sid),收敛
284
+ }
285
+ }
286
+
261
287
  function fetchTasks() {
262
288
  if (!state.sessionId) return
263
289
  var epoch = reqEpoch
@@ -265,7 +291,17 @@
265
291
  if (epoch !== reqEpoch) return // 会话已切换,丢弃过期响应
266
292
  if (d && d.error) { reportReadErr('看板数据刷新失败:' + d.error); return } // error 分支:保留旧 tasks/设置,绝不当空板渲染
267
293
  clearReadErr()
268
- state.tasks = (d && d.tasks) || []
294
+ // 渲染节约(反馈 n-mut9rzs2mkhg):host 附 tasksHash(任务关键字段的稳定 hash,口径见 host core.tasksHash)。
295
+ // hash 相同 → 任务列表实质没变:跳过 state.tasks 赋值与 notify(3s 轮询大头是空转重渲染)。
296
+ // 注意区分:tasksErr 清除、boardMode/usageSummary/healthHints/childStats 等轻量字段照常赋值
297
+ // (hash 只管任务列表渲染;这些字段随下次任意 notify 生效,不为它们单独渲染)。
298
+ // 老 host 不返回 tasksHash → 恒视为变化,行为与旧版完全一致。
299
+ var newHash = (d && d.tasksHash) || ''
300
+ var tasksChanged = !newHash || newHash !== state.tasksHash
301
+ if (tasksChanged) {
302
+ state.tasksHash = newHash
303
+ state.tasks = (d && d.tasks) || []
304
+ }
269
305
  state.boardMode = (d && d.boardMode) || 'auto'
270
306
  state.teamMode = !!(d && d.teamMode)
271
307
  // 工作模式(三档)读取:优先用服务端派生字段 workMode,缺失时按同口径本地兜底派生
@@ -289,14 +325,22 @@
289
325
  state.childStats = (d && d.childStats) || {}
290
326
  // 学习飞轮 v1 能力检测:老 host 不返回该字段 → 视为开启(默认开);只有显式 false 才关。
291
327
  state.feedbackEnabled = !(d && d.feedbackEnabled === false)
292
- state.isRoot = !d || d.isRoot !== false
293
- if (state.isRoot === false && state.open) { state.open = false; state.detailId = null } // 子代理会话:强制收起看板
328
+ // 回执开关(设置区「通知」):同口径——老 host 不返回 → 视为开,只有显式 false 才关
329
+ state.notifyDispatch = !(d && d.notifyDispatch === false)
330
+ state.notifyDone = !(d && d.notifyDone === false)
331
+ // 史诗拆分总开关(设置区「功能」):同上——老 host 不返回 = 开(引导照旧),只有显式 false 才关
332
+ state.epicSplit = !(d && d.epicSplit === false)
333
+ applyIsRoot(!d || d.isRoot !== false) // 原始值只喂给防抖器,消费点一律读 isRootStable
334
+ if (!state.isRootStable && state.open) { state.open = false; state.detailId = null } // 子代理会话(含连续 3 次 false 的真降级):强制收起看板
294
335
  if (d && d.dispatchInfo && d.dispatchInfoAt && Date.now() - new Date(d.dispatchInfoAt).getTime() < 120000) { state.dispatchInfo = d.dispatchInfo } else { state.dispatchInfo = '' } // 瞬时通知 2min 内有效,过期强制清空(服务端写后不清曾致残留数天)
295
336
  // escalation 一等公民:出现新的待裁决任务 → 面板自动弹开直达该任务详情
296
- var newEsc = state.tasks.filter(function (t) { return t.escalation && state.escalatedIds.indexOf(t.id) < 0 })
297
- state.escalatedIds = state.tasks.filter(function (t) { return t.escalation }).map(function (t) { return t.id })
298
- if (newEsc.length > 0) { state.open = true; state.view = 'board'; state.detailId = newEsc[0].id }
299
- notify()
337
+ // (escalation.question 在 tasksHash 序列化口径内:新歧义必然改变 hash → 必然走变化分支,不会漏弹)
338
+ if (tasksChanged) {
339
+ var newEsc = state.tasks.filter(function (t) { return t.escalation && state.escalatedIds.indexOf(t.id) < 0 })
340
+ state.escalatedIds = state.tasks.filter(function (t) { return t.escalation }).map(function (t) { return t.id })
341
+ if (newEsc.length > 0) { state.open = true; state.view = 'board'; state.detailId = newEsc[0].id }
342
+ notify()
343
+ }
300
344
  }).catch(function (e) { if (epoch !== reqEpoch) return; reportReadErr('看板数据刷新失败:' + readErrText(e)) }) // 网络 reject 同口径
301
345
  }
302
346
 
@@ -305,7 +349,7 @@
305
349
 
306
350
  // 活动心跳:对进行中/验收中的任务轮询子代理最近动作(卡片与详情展示"现在跑到哪了")
307
351
  function fetchActivity() {
308
- if (!state.sessionId || !state.isRoot || !state.open) return // 面板关闭时不轮询活动(省同步 I/O)
352
+ if (!state.sessionId || !state.isRootStable || !state.open) return // 面板关闭时不轮询活动(省同步 I/O);isRootStable 口径见 isRoot 蝶变防抖
309
353
  var running = state.tasks.filter(function (t) { return t.status === 'in-progress' || t.status === 'verifying' })
310
354
  if (!running.length) { if (Object.keys(state.activity).length) { state.activity = {}; notify() } return }
311
355
  var pending = running.length
@@ -456,9 +500,9 @@
456
500
  function BoardButton(props) {
457
501
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect
458
502
  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]
459
- 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])
460
- 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) } }, [])
461
- if (!isRoot) return null // 子代理会话不显示看板入口
503
+ useEffect(function () { if (props && props.sessionId) { var sid = String(props.sessionId); if (state.sessionId !== sid) { state.sessionId = sid; reqEpoch++; state.tasks = []; state.tasksHash = ''; state.children = []; state.activity = {}; state.archived = []; state.detailId = null; state.childStats = {}; state.tasksErr = ''; state.isRootEverTrue = false; state.isRootFalseN = 0; state.isRootStable = false; notify(); fetchTasks(); fetchChildren() } } }, [props && props.sessionId]) // 会话切换重置 isRoot 防抖态(everTrue/计数/stable 都按会话生命周期——旧会话的"曾确认 true"不得漂到新会话);reset 后首轮 get-tasks 判定前按"未确认 root"保守隐藏,避免切进子代理会话时入口闪现
504
+ 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.isRootStable) }; listeners.push(update); update(); return function () { var i = listeners.indexOf(update); if (i >= 0) listeners.splice(i, 1) } }, [])
505
+ if (!isRoot) return null // 子代理会话不显示看板入口(读 isRootStable:瞬态 false 不收,见 isRoot 蝶变防抖)
462
506
  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)
463
507
  }
464
508
 
@@ -474,9 +518,11 @@
474
518
  var _te = useState(state.tasksErr), tasksErr = _te[0], setTasksErr = _te[1] // 读路径错误条(无错不渲染)
475
519
  var _sto = useState(state.softTimeoutMin), softT = _sto[0], setSoftT = _sto[1]; var _hto = useState(state.hardTimeoutMin), hardT = _hto[0], setHardT = _hto[1]
476
520
  var _fbo = useState(state.feedbackEnabled), fbEnabled = _fbo[0], setFbEnabled = _fbo[1]
477
- 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) } }, [])
521
+ var _ndo = useState(state.notifyDispatch), ndOn = _ndo[0], setNdOn = _ndo[1]; var _nno = useState(state.notifyDone), nnOn = _nno[0], setNnOn = _nno[1]
522
+ var _eso = useState(state.epicSplit), esOn = _eso[0], setEsOn = _eso[1] // 史诗拆分总开关勾选态(设置区「功能」)
523
+ 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); 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) } }, [])
478
524
  if (!open) return null
479
- if (state.isRoot === false) return null // 子代理会话不渲染看板面板
525
+ if (!state.isRootStable) return null // 子代理会话不渲染看板面板(读 isRootStable:瞬态 false 不闪,见 isRoot 蝶变防抖)
480
526
  var active = tasks.filter(function (t) { return t.status !== 'archived' }); var archived = tasks.filter(function (t) { return t.status === 'archived' })
481
527
  var hasFilter = state.filterQ.trim() || state.filterPrio.length > 0 || state.filterTag
482
528
  if (hasFilter) { active = active.filter(passFilter); archived = archived.filter(passFilter) }
@@ -500,7 +546,7 @@
500
546
  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 } },
501
547
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } }, React.createElement('span', { style: { fontWeight: 600, fontSize: 13, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 5 } }, ic('clipboard-list', 15), '智能看板'), React.createElement(ViewTab, null), React.createElement(PoolStatus, null)),
502
548
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
503
- React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled }),
549
+ React.createElement(PoolCfgPopover, { minW: minW, maxW: maxW, minV: minV, maxV: maxV, workerModel: workerModel, verifierModel: verifierModel, softT: softT, hardT: hardT, feedbackEnabled: fbEnabled, notifyDispatch: ndOn, notifyDone: nnOn, epicSplit: esOn }),
504
550
  dispatchInfo ? React.createElement('span', { style: { fontSize: 9, color: C.brand, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: dispatchInfo }, dispatchInfo) : null,
505
551
  React.createElement('button', { onClick: function () { state.createOpen = true; notify() }, title: '新建任务(可存为草稿)', style: { fontSize: 11, padding: '3px 8px', border: '1px solid ' + C.border, borderRadius: 6, cursor: 'pointer', background: 'transparent', color: C.text2, display: 'inline-flex', alignItems: 'center', gap: 3 } }, ic('plus', 11), '新建任务'),
506
552
  createFlash ? React.createElement('span', { style: { fontSize: 10, color: C.ok } }, createFlash) : null,
@@ -69,30 +69,155 @@
69
69
 
70
70
  // 父子区块(与 DepsSection 并列):
71
71
  // 子卡 = 父链面包屑「↳ 史诗:<父标题>」,点击回跳父卡详情;
72
- // 父卡 = 非归档子任务清单(状态色点 + 标题 + 状态标签,点击直达子卡详情;标题行汇总 resolved/total)。
72
+ // 父卡 = 子任务清单(状态色点 + 标题 + 状态标签,点击直达子卡详情;标题行汇总 settled/total)。
73
+ // 归档留档(反馈 task-muupgfot):清单**含已归档子任务**——归档后子任务不该从详情消失,
74
+ // 否则史诗拆分过程整段丢失、只剩一个分母缩水的数字。归档行灰化(降透明度 + 次要文字色)
75
+ // + 行尾「已归档」徽章,并沉底排在未归档之后(稳定排序:未归档保持 state.tasks 既有顺序);
76
+ // 仅降视觉权重不改交互——点击照旧进子卡详情看留档。
77
+ // 口径与卡片 📦 徽章同源:settled = resolved | cancelled | archived(宿主 aggregateChildStats 的
78
+ // settled/resolved 字段),total 含归档,所以详情与卡片永远显示同一个 N/M。
73
79
  // 数据源是 state.tasks 按 parentId 现算(parentId 为老字段),不依赖 host 的 childStats,天然缺省兼容;
74
80
  // 既无父也无子的普通卡整块不渲染。嵌套史诗(本身也是子卡的父卡)两段同区块上下排列。
75
81
  function FamilySection(props) {
76
82
  var task = props.task
77
83
  var parentT = task.parentId ? getTask(task.parentId) : null
78
- var kids = state.tasks.filter(function (x) { return x.parentId === task.id && x.status !== 'archived' })
84
+ // 不按状态过滤(归档也要留在清单里);仅按「是否归档」稳定排序把归档沉底(V8 sort 稳定,未归档保序)
85
+ var kids = state.tasks.filter(function (x) { return x.parentId === task.id })
86
+ .sort(function (a, b) { return (a.status === 'archived' ? 1 : 0) - (b.status === 'archived' ? 1 : 0) })
79
87
  if (!task.parentId && kids.length === 0) return null
80
- var resolvedN = kids.filter(function (x) { return x.status === 'resolved' }).length
88
+ var settledN = kids.filter(function (x) { return x.status === 'resolved' || x.status === 'cancelled' || x.status === 'archived' }).length
81
89
  function jump(id) { state.detailId = id; notify() }
82
90
  return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
83
91
  task.parentId ? React.createElement('div', { style: { fontSize: 11, marginBottom: kids.length ? 6 : 0 } },
84
92
  React.createElement('span', { style: { color: C.text2, fontWeight: 600 } }, '↳ 史诗:'),
85
93
  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,
86
94
  kids.length > 0 ? React.createElement('div', null,
87
- React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '📦 子任务 · ' + resolvedN + '/' + kids.length + ' 已完成'),
95
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '📦 子任务 · ' + settledN + '/' + kids.length + ' 已了结'),
88
96
  kids.map(function (x) {
89
- 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' } },
97
+ var arch = x.status === 'archived'
98
+ return React.createElement('div', { key: x.id, onClick: function () { jump(x.id) }, title: (statusLabels[x.status] || x.status) + ' · ' + x.id + (arch ? ' · 已归档留档(点击查看)' : ''), style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3, fontSize: 10, cursor: 'pointer', opacity: arch ? 0.55 : 1 } },
90
99
  React.createElement('span', { style: { width: 7, height: 7, borderRadius: '50%', background: statusColors[x.status] || C.text2, flexShrink: 0 } }),
91
- React.createElement('span', { style: { color: C.brand, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, x.title || x.id),
92
- React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0 } }, statusLabels[x.status] || x.status))
100
+ React.createElement('span', { style: { color: arch ? C.text2 : C.brand, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, x.title || x.id),
101
+ arch
102
+ ? React.createElement('span', { style: { fontSize: 9, padding: '0 4px', borderRadius: 2, background: C.nested, color: C.text2, border: '1px solid ' + C.border, flexShrink: 0 } }, '已归档')
103
+ : React.createElement('span', { style: { fontSize: 9, color: C.text2, flexShrink: 0 } }, statusLabels[x.status] || x.status))
93
104
  })) : null)
94
105
  }
95
106
 
107
+ // ===== Hooks(可选点位)· 史诗前置/后置 agent run 的编辑与状态面板(task-muuw56yf)=====
108
+ // 契约以 host core.normalizeHooks / mergeHooks 为准(勿凭记忆改字段名):
109
+ // epic.hooks = { pre: { enabled, prompt, state, runId, pending? }, post: 同构 },state ∈ idle|running|done|failed;
110
+ // hook run 记在 epic.runs[](role 'hook-pre'/'hook-post',字段 { role, id, at, endedAt, outcome, model })。
111
+ // 写入通道 = update-task RPC 的 hooks 字段(浅合并:未提交的点位保留原值,传 null 撤点位)。
112
+ // 三条 UI 口径:
113
+ // ① 老卡零渲染:既无子任务、也未声明 hooks 的普通卡整块不出现(与卡片相位徽章同一门禁);
114
+ // ② 权限不在 UI 判断——「hooks 仅主窗口可设」由 host 双重门禁裁决,失败原因原样 ⚠️ 回显;
115
+ // 客户端预判角色只会把真实错误变成灰按钮,反而更难查;
116
+ // ③ state 只读展示(host 状态机推进,重启可恢复);重试=把 state 置回 idle,走裁决通道,
117
+ // 这里只负责给 failed 一个「前往裁决」锚点,不另造裁决入口。
118
+ function hookRunOf(task, phase, runId) {
119
+ var role = 'hook-' + phase
120
+ // host 侧 run id 落位不对称:pre 在 spawn 成功时回填 hooks.pre.runId,post 复用 verifierRun 位
121
+ // (claim 占位换真 id);且两端结算都把 runId 归零——所以取不到时按角色回退「最后一条」。
122
+ var rid = runId || (phase === 'post' ? task.verifierRun : null)
123
+ var runs = Array.isArray(task.runs) ? task.runs : []
124
+ var hit = null
125
+ for (var i = 0; i < runs.length; i++) {
126
+ if (!runs[i] || runs[i].role !== role) continue
127
+ if (rid && runs[i].id !== rid) continue
128
+ hit = runs[i] // 顺序遍历取最后一条匹配:重试后的新 run 覆盖旧展示
129
+ }
130
+ return hit
131
+ }
132
+ function hookStateMeta(task, phase) {
133
+ var h = (task.hooks && task.hooks[phase]) || null
134
+ var st = (h && h.state) || 'idle'
135
+ var off = !h || !h.enabled
136
+ var run = hookRunOf(task, phase, h && h.runId)
137
+ var meta = { idle: { label: off ? '未启用' : '待运行', color: C.text2 }, running: { label: '运行中', color: C.brand }, done: { label: '已完成', color: C.ok }, failed: { label: '失败', color: C.err } }[st] || { label: String(st), color: C.text2 }
138
+ // 耗时:运行中 = 距 spawn 的已跑时长;已落定 = spawn → endedAt 净耗时(老 run 无 endedAt 则不显示)
139
+ var dur = ''
140
+ if (run && run.at) {
141
+ if (st === 'running') dur = '已跑 ' + elapsedSince(run.at)
142
+ else if (run.endedAt) dur = '耗时 ' + fmtDur(new Date(run.at).getTime(), new Date(run.endedAt).getTime())
143
+ }
144
+ return { state: st, off: off, label: meta.label, color: meta.color, run: run, dur: dur }
145
+ }
146
+ function HooksSection(props) {
147
+ var _R = React; var useState = _R.useState // 与全库组件同例:bundler 只注入 React 本体,hook 须自行解构(漏了就是运行期 ReferenceError,shell 容错层直接卸载整个 overlay——2026-10-05 详情页崩板事故)
148
+ var task = props.task
149
+ var kHooks = task.hooks || null
150
+ var _a = useState(''), msg = _a[0], setMsg = _a[1]
151
+ var _b = useState(false), saving = _b[0], setSaving = _b[1]
152
+ // 编辑缓冲以「卡上现值」为初值;父层以 key=task.id 挂载 → 切换任务自动重挂,缓冲不会串卡
153
+ var _c = useState({ pre: { enabled: !!(kHooks && kHooks.pre && kHooks.pre.enabled), prompt: (kHooks && kHooks.pre && kHooks.pre.prompt) || '' }, post: { enabled: !!(kHooks && kHooks.post && kHooks.post.enabled), prompt: (kHooks && kHooks.post && kHooks.post.prompt) || '' } }), buf = _c[0], setBuf = _c[1]
154
+ var kids = state.tasks.filter(function (x) { return x.parentId === task.id })
155
+ var declared = !!(kHooks && (kHooks.pre || kHooks.post))
156
+ if (kids.length === 0 && !declared) return null
157
+ function setPhase(ph, patch) { var nb = { pre: buf.pre, post: buf.post }; nb[ph] = Object.assign({}, nb[ph], patch); setBuf(nb) }
158
+ // 提交体组装(三条口径缺一都踩坑):
159
+ // a) prompt 清空 = 撤掉该点位:host normalizeHooks 明确拒绝空 prompt,撤点位用 null 表达才在契约内
160
+ // (不该把「我不想要这个 hook 了」变成一条报错);**例外**——该点位 state='running' 时不清空:
161
+ // run 还在飞,撤掉声明会让它的结算落到空点位(表现为白跑一轮),删除意图原样保留并回一行提示;
162
+ // b) 已声明点位的 state/runId/pending 原样回传:normalizeHooks 对缺省 state 归零成 idle,
163
+ // 不透传就等于「改一下 prompt 把在跑/已落定的 hook 打回待运行」;
164
+ // c) 凑不出 prompt 的点位一律提交 null(幂等删除,不凭空造点位)。
165
+ function payloadFor(ph) {
166
+ var cur = (task.hooks && task.hooks[ph]) || null
167
+ var b = buf[ph] || { enabled: false, prompt: '' }
168
+ function keep(h) { var it = { enabled: !!h.enabled, prompt: String(h.prompt || '') }; if (h.state) it.state = h.state; if (h.runId) it.runId = h.runId; if (h.pending) it.pending = true; return it }
169
+ if (!String(b.prompt || '').trim()) {
170
+ if (cur && cur.state === 'running') return { item: keep(cur), warn: ph + ' 运行中:本次不清空该点位(run 还在跑),等它跑完再撤' }
171
+ return { item: null }
172
+ }
173
+ var item = { enabled: !!b.enabled, prompt: String(b.prompt).slice(0, 4000) }
174
+ if (cur) {
175
+ if (cur.state) item.state = cur.state
176
+ if (cur.runId) item.runId = cur.runId
177
+ if (cur.pending) item.pending = true
178
+ }
179
+ return { item: item }
180
+ }
181
+ function save() {
182
+ if (saving) return
183
+ var pre = payloadFor('pre'), post = payloadFor('post')
184
+ var warns = [pre.warn, post.warn].filter(Boolean)
185
+ setSaving(true); setMsg('')
186
+ rpc('update-task', { taskId: task.id, hooks: { pre: pre.item, post: post.item } }).then(function (r) {
187
+ setSaving(false)
188
+ if (r && r.ok === false) { setMsg('⚠️ ' + (r.error || '保存失败')); return }
189
+ setMsg(warns.length ? '⚠️ ' + warns.join(';') : '✅ 已保存 hooks 配置')
190
+ fetchTasks()
191
+ }).catch(function (e) { setSaving(false); setMsg('⚠️ ' + String(e)) })
192
+ }
193
+ // 「前往裁决」= 同页锚点跳转(裁决区 id 固定 tskb-escalation,由 DetailView 渲染);
194
+ // hook 失败时 host 已把 epic 转 blocked 并挂好 escalation,这里只把视线送过去。
195
+ function goArbitration() { var el = document.getElementById('tskb-escalation'); if (el && el.scrollIntoView) el.scrollIntoView({ block: 'center' }) }
196
+ function phaseRow(ph, title, hint) {
197
+ var m = hookStateMeta(task, ph)
198
+ var b = buf[ph] || { enabled: false, prompt: '' }
199
+ return React.createElement('div', { key: ph, style: { marginBottom: 6, padding: '5px 7px', border: '1px solid ' + C.border, borderRadius: 5, background: C.nested } },
200
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', marginBottom: 4 } },
201
+ React.createElement('label', { style: { fontSize: 11, fontWeight: 600, color: C.text, display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer' } },
202
+ React.createElement('input', { type: 'checkbox', checked: !!b.enabled, onChange: function (e) { setPhase(ph, { enabled: e.target.checked }) }, title: '启用该点位(停用后 host 全链路跳过它)' }),
203
+ title),
204
+ React.createElement('span', { title: 'state 由 host 状态机推进(写在卡上,重启可恢复);失败后重试=把 state 置回 idle,走裁决通道', style: { fontSize: 9, padding: '0 5px', borderRadius: 3, background: 'color-mix(in srgb, ' + m.color + ' 16%, transparent)', color: m.color, fontWeight: 600 } }, m.label),
205
+ m.dur ? React.createElement('span', { style: { fontSize: 9, color: C.text2 } }, m.dur) : null,
206
+ m.run && m.run.id ? React.createElement('button', { onClick: function () { if (uiWorkspaceSvc) uiWorkspaceSvc.openSession(m.run.id) }, title: '查看该 hook run 的会话(' + m.run.id + (m.run.model ? ' · ' + m.run.model : '') + ')', style: { fontSize: 9, padding: '1px 6px', border: '1px solid ' + C.brand, borderRadius: 3, background: 'transparent', color: C.brand, cursor: 'pointer' } }, '→ 查看会话') : null,
207
+ m.state === 'failed' ? React.createElement('button', { onClick: goArbitration, title: '跳到本页裁决区:重试(置回 idle)/ 跳过(置 done 放行)/ 放弃(终止史诗)', style: { fontSize: 9, padding: '1px 6px', border: 'none', borderRadius: 3, background: C.err, color: C_INV, cursor: 'pointer', fontWeight: 600 } }, '前往裁决') : null),
208
+ React.createElement('textarea', { value: b.prompt, onChange: function (e) { setPhase(ph, { prompt: e.target.value }) }, rows: 2, placeholder: '补充指令(可选)——前置=让子任务具备开跑条件;后置=把这批活收口。具体动作由 hook agent 自行决策', style: { width: '100%', fontSize: 11, padding: '4px 6px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' } }),
209
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 3, lineHeight: 1.6 } }, hint))
210
+ }
211
+ return React.createElement('div', { style: { marginBottom: 8, padding: '6px 8px', border: '1px solid ' + C.border, borderRadius: 6, background: C.card } },
212
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('zap', 11), 'Hooks(可选点位)'),
213
+ phaseRow('pre', '⏳ 前置(pre)', '让子任务具备开跑条件:未完成前该史诗的子任务一张都不派(串行闸门)。'),
214
+ phaseRow('post', '🧪 收尾(post)', '把这批活收口:全部子任务了结后跑一遍,完成后史诗才转验证中。'),
215
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
216
+ React.createElement('button', { onClick: save, disabled: saving, title: '保存到卡上(update-task RPC,hooks 字段浅合并)', style: { fontSize: 10, padding: '3px 8px', border: 'none', borderRadius: 3, background: C.brand, color: C_INV, cursor: saving ? 'default' : 'pointer', opacity: saving ? 0.6 : 1, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 3 } }, saving ? '保存中…' : [ic('save', 10), ' 保存 hooks']),
217
+ msg ? React.createElement('span', { style: { fontSize: 10, color: C.text2 } }, msg) : null),
218
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 4, lineHeight: 1.6 } }, '每个点位是一次真实 agent 运行(不是声明式命令):prompt 只给薄框架契约与上下文,动作由 hook agent 自行决策,吃不准会歧义上报;失败即转阻塞等人裁决(重试/跳过/放弃),不会自动重跑。清空 prompt 保存 = 撤掉该点位。hooks 仅主窗口可设(host 门禁,失败原因原样回显)。'))
219
+ }
220
+
96
221
  function DetailView() {
97
222
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
98
223
  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]
@@ -115,7 +240,7 @@
115
240
  if (r && r.ok === false && r.error === 'touches-conflict') {
116
241
  var ids = Array.isArray(r.conflicts) ? r.conflicts : []
117
242
  var names = ids.map(function (id) { var t2 = getTask(id); return (t2 ? t2.title : id) + ' (' + id + ')' }).join('、')
118
- if (window.confirm('⚠️ 文件锁冲突:以下进行中任务正在改同一批文件(touches 重叠):\n\n' + (names || ids.join('、')) + '\n\n强行并行可能互相覆盖改动/diff 冲突。仍要越权派发吗?')) return send(true)
243
+ if (window.confirm('⚠️ 文件锁冲突:以下任务正持有同一批文件(touches 重叠;锁持到归档——verifying/resolved 卡也在持锁,等其归档即自动放行):\n\n' + (names || ids.join('、')) + '\n\n强行并行可能互相覆盖改动/diff 冲突。仍要越权派发吗?')) return send(true)
119
244
  setActionMsg('⛔ 已取消派发(等文件锁释放,或调整 touches 声明)')
120
245
  return { ok: false, cancelled: true }
121
246
  }
@@ -125,9 +250,34 @@
125
250
  function submitIntervene() { if (!interveneMsg.trim()) return; rpc('intervene-agent', { taskId: task.id, message: interveneMsg }).then(function (r) { setActionMsg(r && r.ok ? '✅ 高优指令已插入 ' + shortId(r.agent) + ' 队首' : '⚠️ ' + ((r && r.error) || '无活动 Agent')); setInterveneMsg(''); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) }) }
126
251
  var isManual = task.assignMode === 'manual'; var canJump = (task.status === 'in-progress' || task.status === 'verifying') && task.claimedBy && task.claimedBy !== state.sessionId
127
252
  var canIntervene = task.status === 'in-progress' || task.status === 'verifying'
253
+ // ===== 无障碍·「流转到」按钮组(反馈 n-mut9rzpyc7p1):状态流转不再只有拖拽一条路 =====
254
+ // 合法迁移与拖拽 transition()(kernel.js)逐条对齐——host 门禁不变,失败原因 ⚠️ 回显 actionMsg 行;
255
+ // 原生 button 元素天然键盘可达(Tab 聚焦 + Enter/Space 触发),读屏可报组名与按钮名
256
+ var FLOW_DEF = {
257
+ 'pending': [{ to: 'in-progress', label: '▶ 开始处理', color: C.brand, tip: '领取并开始处理(待办 → 进行中)' }],
258
+ 'in-progress': [{ to: 'verifying', label: '✅ 提交验收', color: C.ok, tip: '提交验证(进行中 → 验证中),需填解决说明' }, { to: 'blocked', label: '⛔ 标记阻塞', color: C.warn, tip: '标记为阻塞(进行中 → 阻塞)' }],
259
+ 'blocked': [{ to: 'pending', label: '↩ 重投待办', color: C.brand, tip: '解除阻塞重新投放(阻塞 → 待办)' }, { to: 'in-progress', label: '▶ 开始处理', color: C.brand, tip: '领取并开始处理(阻塞 → 进行中)' }],
260
+ 'verifying': [{ to: 'resolved', label: '✔ 验收通过', color: C.ok, tip: '验收通过(验证中 → 已完成)' }, { to: 'in-progress', label: '↩ 驳回重投', color: C.err, tip: '驳回回执行中(验证中 → 进行中)' }]
261
+ }
262
+ var flowBtns = FLOW_DEF[task.status] || []
263
+ function flowTo(to) {
264
+ var p = null
265
+ if (to === 'in-progress' && (task.status === 'pending' || task.status === 'blocked')) p = rpc('claim-task', { taskId: task.id })
266
+ else if (to === 'pending' && task.status === 'blocked') p = rpc('update-task', { taskId: task.id, resetToPending: true })
267
+ else if (to === 'verifying' && task.status === 'in-progress') { var res = window.prompt('提交验证 — 解决说明(必填):'); if (!res) return; p = rpc('resolve-task', { taskId: task.id, status: 'verifying', resolution: res }) }
268
+ else if (to === 'blocked' && task.status === 'in-progress') { var rs = window.prompt('阻塞原因(可选):') || ''; p = rpc('resolve-task', { taskId: task.id, status: 'blocked', resolution: rs }) }
269
+ else if (to === 'resolved' && task.status === 'verifying') p = rpc('verify-task', { taskId: task.id, verdict: 'approved' })
270
+ else if (to === 'in-progress' && task.status === 'verifying') { var cm = window.prompt('驳回原因(可选):'); p = rpc('verify-task', { taskId: task.id, verdict: 'rejected', comment: cm || '' }) }
271
+ if (!p) return
272
+ p.then(function (r) { if (r && r.ok === false) setActionMsg('⚠️ 流转失败:' + (r.error || '未知错误')); else setActionMsg('✅ 已流转到「' + (statusLabels[to] || to) + '」'); fetchTasks() }).catch(function (e) { setActionMsg('⚠️ ' + String(e)) })
273
+ }
128
274
  return React.createElement('div', { style: { padding: '4px 2px' } },
129
275
  React.createElement('div', { onClick: function () { state.detailId = null; notify() }, style: { fontSize: 11, color: C.brand, cursor: 'pointer', marginBottom: 8 } }, '← 返回看板'),
130
276
  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),
277
+ // 流转到按钮组(键盘可达的状态迁移入口,替代拖拽;无合法迁移的状态(draft/resolved 等)整块不渲染)
278
+ 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 } },
279
+ React.createElement('span', { style: { fontSize: 10, fontWeight: 600, color: C.text2 } }, '⇄ 流转到:'),
280
+ flowBtns.map(function (b) { return React.createElement('button', { key: b.to, onClick: function () { flowTo(b.to) }, title: b.tip, style: { fontSize: 10, padding: '3px 9px', border: '1px solid ' + b.color, borderRadius: 3, background: 'transparent', color: b.color, cursor: 'pointer', fontWeight: 600 } }, b.label) })) : null,
131
281
  // 最近失败(host settle 失败路径写入 task.lastError;缺字段静默不渲染,≤2 行截断,悬停看全文)
132
282
  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,
133
283
  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' } },
@@ -146,7 +296,7 @@
146
296
  React.createElement('span', null, '已运行 ' + elapsedSince(task.claimedAt) + '(软超时 ' + state.softTimeoutMin + ' 分提醒 · 硬超时 ' + state.hardTimeoutMin + ' 分自动终止)'),
147
297
  elapsedMin(task.claimedAt) > state.softTimeoutMin ? React.createElement('span', { style: { color: C.warn, fontWeight: 600 } }, '⏱ 已超软超时') : null,
148
298
  React.createElement('button', { onClick: doTerminate, title: '终止该执行 Agent,任务回 pending 重新排队', style: { fontSize: 10, padding: '2px 8px', border: '1px solid ' + C.err, borderRadius: 3, background: 'transparent', color: C.err, cursor: 'pointer', marginLeft: 'auto' } }, '⏹ 立即终止')) : null,
149
- task.escalation ? React.createElement('div', { style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
299
+ task.escalation ? React.createElement('div', { id: 'tskb-escalation', style: { marginBottom: 8, padding: '8px 10px', border: '1px solid ' + C.err, borderRadius: 6, background: 'color-mix(in srgb, ' + C.err + ' 8%, transparent)' } },
150
300
  React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: C.err, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4 } }, ic('alert-triangle', 13), 'Worker 上报歧义 — 等待主窗口裁决'),
151
301
  React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, whiteSpace: 'pre-wrap', maxHeight: 160, overflowY: 'auto' } }, task.escalation.question),
152
302
  React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 6 } }, '上报于 ' + ago(task.escalation.at) + ' · ' + (task.escalation.by || '')),
@@ -189,6 +339,8 @@
189
339
  '📎 调研注入: ' + (empty ? '无调研注入——Worker 需自行定位,建议补 contextFiles/contextNotes' : parts.join(' · ')))
190
340
  })(),
191
341
  React.createElement(FamilySection, { task: task }),
342
+ // Hooks 区(史诗可选点位):key=task.id 保证切换任务时重挂,编辑缓冲不串卡
343
+ React.createElement(HooksSection, { key: task.id, task: task }),
192
344
  React.createElement(DepsSection, { task: task }),
193
345
  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,
194
346
  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,