@goodandready/dsh-kanban 0.2.32 → 0.2.33
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 +261 -1
- package/lib/config.js +4 -0
- package/lib/cost-telemetry.js +146 -0
- package/lib/index.js +29 -1
- package/lib/routes.js +88 -3
- package/lib/store.js +22 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1266,6 +1266,29 @@ window.__ModuleLoader__.load({
|
|
|
1266
1266
|
'error.crossSite': 'Request rejected as cross-site.',
|
|
1267
1267
|
'error.timeout': 'Request timed out. Please try again.',
|
|
1268
1268
|
'error.unknown': 'Request failed.',
|
|
1269
|
+
'field.costMeterEnabled': 'Cost Meter Telemetry',
|
|
1270
|
+
'hint.costMeterEnabled': 'Enable token and cost telemetry integration via @goodandready/dsh-cost-meter.',
|
|
1271
|
+
'field.taskCostLimit': 'Task Cost Limit (USD)',
|
|
1272
|
+
'hint.taskCostLimit': 'Cost threshold per task in USD; 0 disables threshold warnings.',
|
|
1273
|
+
'card.costLimitExceeded': 'Cost limit exceeded',
|
|
1274
|
+
'card.nudge': 'Nudge',
|
|
1275
|
+
'card.nudgeHint': 'Send live instruction to running agent',
|
|
1276
|
+
'nudge.title': 'Live Steering',
|
|
1277
|
+
'nudge.placeholder': 'Enter immediate instruction or correction for the agent...',
|
|
1278
|
+
'nudge.send': 'Send',
|
|
1279
|
+
'nudge.sending': 'Sending...',
|
|
1280
|
+
'nudge.success': 'Instruction delivered to agent',
|
|
1281
|
+
'nudge.failed': 'Failed to deliver instruction',
|
|
1282
|
+
'costMeter.notice': 'Cost and token tracking requires @goodandready/dsh-cost-meter to be installed and active in DSH profile.',
|
|
1283
|
+
'costMeter.active': 'Plugin @goodandready/dsh-cost-meter is active',
|
|
1284
|
+
'costMeter.inactive': 'Plugin @goodandready/dsh-cost-meter not detected',
|
|
1285
|
+
'telemetry.title': 'Resource Telemetry',
|
|
1286
|
+
'telemetry.tokens': 'Tokens',
|
|
1287
|
+
'telemetry.input': 'Input',
|
|
1288
|
+
'telemetry.output': 'Output',
|
|
1289
|
+
'telemetry.cache': 'Cached',
|
|
1290
|
+
'telemetry.cost': 'Estimated Cost',
|
|
1291
|
+
'telemetry.limit': 'Budget Limit',
|
|
1269
1292
|
}
|
|
1270
1293
|
|
|
1271
1294
|
const zh = {
|
|
@@ -1667,6 +1690,29 @@ window.__ModuleLoader__.load({
|
|
|
1667
1690
|
'error.crossSite': '请求被判定为跨站而拒绝。',
|
|
1668
1691
|
'error.timeout': '请求超时,请重试。',
|
|
1669
1692
|
'error.unknown': '请求失败。',
|
|
1693
|
+
'field.costMeterEnabled': '成本计量遥测',
|
|
1694
|
+
'hint.costMeterEnabled': '通过 @goodandready/dsh-cost-meter 启用 Token 和成本遥测集成。',
|
|
1695
|
+
'field.taskCostLimit': '任务成本限额 (USD)',
|
|
1696
|
+
'hint.taskCostLimit': '每个任务的成本阈值(美元);0 表示禁用超额警告。',
|
|
1697
|
+
'card.costLimitExceeded': '超出成本限额',
|
|
1698
|
+
'card.nudge': '引导',
|
|
1699
|
+
'card.nudgeHint': '向正在运行的 Agent 发送实时引导指令',
|
|
1700
|
+
'nudge.title': '实时引导',
|
|
1701
|
+
'nudge.placeholder': '输入对 Agent 的即时指令或修正建议...',
|
|
1702
|
+
'nudge.send': '发送',
|
|
1703
|
+
'nudge.sending': '正在发送...',
|
|
1704
|
+
'nudge.success': '指令已发送给 Agent',
|
|
1705
|
+
'nudge.failed': '发送指令失败',
|
|
1706
|
+
'costMeter.notice': 'Token 及成本追踪功能需要 DSH Profile 中安装并激活 @goodandready/dsh-cost-meter 插件。',
|
|
1707
|
+
'costMeter.active': '已激活 @goodandready/dsh-cost-meter 插件',
|
|
1708
|
+
'costMeter.inactive': '未检测到 @goodandready/dsh-cost-meter 插件',
|
|
1709
|
+
'telemetry.title': '资源遥测',
|
|
1710
|
+
'telemetry.tokens': 'Token 统计',
|
|
1711
|
+
'telemetry.input': '输入',
|
|
1712
|
+
'telemetry.output': '输出',
|
|
1713
|
+
'telemetry.cache': '缓存命中',
|
|
1714
|
+
'telemetry.cost': '预估成本',
|
|
1715
|
+
'telemetry.limit': '预算限额',
|
|
1670
1716
|
}
|
|
1671
1717
|
|
|
1672
1718
|
function fallbackT(key, vars) {
|
|
@@ -1715,6 +1761,8 @@ window.__ModuleLoader__.load({
|
|
|
1715
1761
|
{ field: 'verificationGateEnabled', kind: 'bool', group: 'general' },
|
|
1716
1762
|
{ field: 'verificationCommand', kind: 'text', group: 'general' },
|
|
1717
1763
|
{ field: 'verificationTimeoutMs', kind: 'number', group: 'general' },
|
|
1764
|
+
{ field: 'costMeterEnabled', kind: 'bool', group: 'sync' },
|
|
1765
|
+
{ field: 'taskCostLimit', kind: 'number', group: 'limits' },
|
|
1718
1766
|
]
|
|
1719
1767
|
|
|
1720
1768
|
function hintKey(spec) {
|
|
@@ -2014,6 +2062,29 @@ window.__ModuleLoader__.load({
|
|
|
2014
2062
|
else if (!writable) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.readOnly'))
|
|
2015
2063
|
else if (saved) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.saved'))
|
|
2016
2064
|
else note = h('p', { className: 'dkb-note' })
|
|
2065
|
+
const costMeterDetected = Boolean(ctx?.costMeter || (ctx?.registry && Array.from(ctx.registry.keys()).some((k) => k === '@goodandready/dsh-cost-meter' || k?.name === '@goodandready/dsh-cost-meter')))
|
|
2066
|
+
rows.push(h('div', {
|
|
2067
|
+
className: 'dkb-section-card',
|
|
2068
|
+
key: 'cost-meter-notice',
|
|
2069
|
+
style: {
|
|
2070
|
+
borderLeft: '3px solid var(--dsw-alias-state-business-primary, #0969da)',
|
|
2071
|
+
background: 'var(--dsw-alias-bg-layer-1, #f6f8fa)',
|
|
2072
|
+
padding: '12px 14px',
|
|
2073
|
+
marginBottom: '12px',
|
|
2074
|
+
},
|
|
2075
|
+
},
|
|
2076
|
+
h('div', {
|
|
2077
|
+
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' },
|
|
2078
|
+
},
|
|
2079
|
+
h('span', { style: { fontWeight: '600', fontSize: '13px' } }, '🪙 ' + t('telemetry.title')),
|
|
2080
|
+
h('span', {
|
|
2081
|
+
className: 'dkb-section-badge' + (costMeterDetected ? ' dkb-section-badge-ok' : ''),
|
|
2082
|
+
}, costMeterDetected ? t('costMeter.active') : t('costMeter.inactive')),
|
|
2083
|
+
),
|
|
2084
|
+
h('p', { className: 'dkb-hint', style: { margin: 0, fontSize: '12px', lineHeight: '1.4' } },
|
|
2085
|
+
t('costMeter.notice'),
|
|
2086
|
+
),
|
|
2087
|
+
))
|
|
2017
2088
|
rows.push(h(PluginUpdaterSection, { t, key: 'updater' }))
|
|
2018
2089
|
rows.push(h('div', { className: 'dkb-foot', key: 'foot' },
|
|
2019
2090
|
note,
|
|
@@ -2094,6 +2165,20 @@ window.__ModuleLoader__.load({
|
|
|
2094
2165
|
: (task.report?.verification?.status === 'failed'
|
|
2095
2166
|
? h('span', { className: 'dkb-tag', style: { background: 'var(--dsw-alias-state-error-secondary, #ffebe9)', color: 'var(--dsw-alias-state-error-primary, #cf222e)', borderColor: 'var(--dsw-alias-state-error-secondary, #ffc1c0)' }, title: task.report.verification.command || 'Tests failed' }, '✗ FAIL')
|
|
2096
2167
|
: null),
|
|
2168
|
+
task.costMetrics && task.costMetrics.totalTokens > 0
|
|
2169
|
+
? h('span', {
|
|
2170
|
+
className: 'dkb-tag',
|
|
2171
|
+
style: { background: 'var(--dsw-alias-state-business-tertiary, #ddf4ff)', color: 'var(--dsw-alias-state-business-primary, #0969da)', borderColor: 'var(--dsw-alias-state-business-tertiary, #54aeff)' },
|
|
2172
|
+
title: 'Tokens: ' + task.costMetrics.totalTokens.toLocaleString()
|
|
2173
|
+
}, '🪙 ' + (task.costMetrics.totalTokens / 1000).toFixed(1) + 'k' + (task.costMetrics.costFormatted ? ' · ' + task.costMetrics.costFormatted : ''))
|
|
2174
|
+
: null,
|
|
2175
|
+
task.costLimitExceeded
|
|
2176
|
+
? h('span', {
|
|
2177
|
+
className: 'dkb-tag',
|
|
2178
|
+
style: { background: 'var(--dsw-alias-state-warn-secondary, #fff8c5)', color: 'var(--dsw-alias-state-warn-primary, #9a6700)', borderColor: 'var(--dsw-alias-state-warn-secondary, #d4a72c)' },
|
|
2179
|
+
title: 'Budget limit exceeded'
|
|
2180
|
+
}, '⚠️ ' + t('card.costLimitExceeded'))
|
|
2181
|
+
: null,
|
|
2097
2182
|
task.report && task.report.summary
|
|
2098
2183
|
? h('span', { className: 'dkb-tag', title: 'Execution Report' }, '📄 report')
|
|
2099
2184
|
: null,
|
|
@@ -2104,6 +2189,16 @@ window.__ModuleLoader__.load({
|
|
|
2104
2189
|
className: 'dkb-tag', key: l, style: tagStyle(task, l),
|
|
2105
2190
|
}, l))),
|
|
2106
2191
|
h('div', { className: 'dkb-taskTitle' }, task.title),
|
|
2192
|
+
task.state === 'running' && task.liveStep
|
|
2193
|
+
? h('div', {
|
|
2194
|
+
className: 'dkb-liveStep',
|
|
2195
|
+
style: { fontSize: '11px', color: 'var(--dsw-alias-state-business-primary, #0969da)', display: 'flex', alignItems: 'center', gap: '4px', margin: '3px 0' },
|
|
2196
|
+
title: task.liveStep
|
|
2197
|
+
},
|
|
2198
|
+
h('span', { style: { display: 'inline-block' } }, '⟳'),
|
|
2199
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, task.liveStep)
|
|
2200
|
+
)
|
|
2201
|
+
: null,
|
|
2107
2202
|
task.subtasksCount && task.subtasksCount.total > 0
|
|
2108
2203
|
? h('div', { className: 'dkb-progressBar', title: `${task.subtasksCount.completed}/${task.subtasksCount.total}` },
|
|
2109
2204
|
h('div', {
|
|
@@ -2160,6 +2255,26 @@ window.__ModuleLoader__.load({
|
|
|
2160
2255
|
onClick: (e) => { e.stopPropagation(); props.onOpenChat(task) },
|
|
2161
2256
|
}, t('card.toChat'))
|
|
2162
2257
|
: null,
|
|
2258
|
+
(task.column === 'in-progress' || task.col === 'in-progress') && task.sessionId && (task.state === 'running' || task.state === 'idle')
|
|
2259
|
+
? h('button', {
|
|
2260
|
+
type: 'button',
|
|
2261
|
+
className: 'dkb-nudgeBtn',
|
|
2262
|
+
title: t('card.nudgeHint'),
|
|
2263
|
+
style: {
|
|
2264
|
+
fontSize: '11px',
|
|
2265
|
+
padding: '1px 6px',
|
|
2266
|
+
borderRadius: '4px',
|
|
2267
|
+
cursor: 'pointer',
|
|
2268
|
+
border: '1px solid var(--dsw-alias-state-business-tertiary, #54aeff)',
|
|
2269
|
+
background: 'var(--dsw-alias-state-business-tertiary, #ddf4ff)',
|
|
2270
|
+
color: 'var(--dsw-alias-state-business-primary, #0969da)',
|
|
2271
|
+
},
|
|
2272
|
+
onClick: (e) => {
|
|
2273
|
+
e.stopPropagation();
|
|
2274
|
+
if (props.onNudge) props.onNudge(task);
|
|
2275
|
+
}
|
|
2276
|
+
}, '💬 ' + t('card.nudge'))
|
|
2277
|
+
: null,
|
|
2163
2278
|
touched
|
|
2164
2279
|
? h('span', {
|
|
2165
2280
|
className: task.stale ? 'dkb-countOver' : '',
|
|
@@ -2317,6 +2432,32 @@ window.__ModuleLoader__.load({
|
|
|
2317
2432
|
const [agentPreset, setAgentPreset] = React.useState('')
|
|
2318
2433
|
const [permission, setPermission] = React.useState('')
|
|
2319
2434
|
const dragged = React.useRef(null)
|
|
2435
|
+
const [nudgeTask, setNudgeTask] = React.useState(null)
|
|
2436
|
+
const [nudgePrompt, setNudgePrompt] = React.useState('')
|
|
2437
|
+
const [nudgeSending, setNudgeSending] = React.useState(false)
|
|
2438
|
+
const [nudgeSuccess, setNudgeSuccess] = React.useState(false)
|
|
2439
|
+
|
|
2440
|
+
const handleNudgeSubmit = React.useCallback(async () => {
|
|
2441
|
+
if (!nudgeTask || !nudgePrompt.trim() || nudgeSending) return
|
|
2442
|
+
setNudgeSending(true)
|
|
2443
|
+
try {
|
|
2444
|
+
await api('/task/' + encodeURIComponent(nudgeTask.id) + '/nudge', {
|
|
2445
|
+
method: 'POST',
|
|
2446
|
+
body: JSON.stringify({ prompt: nudgePrompt.trim() }),
|
|
2447
|
+
})
|
|
2448
|
+
setNudgeSuccess(true)
|
|
2449
|
+
setTimeout(() => {
|
|
2450
|
+
setNudgeTask(null)
|
|
2451
|
+
setNudgePrompt('')
|
|
2452
|
+
setNudgeSuccess(false)
|
|
2453
|
+
}, 1200)
|
|
2454
|
+
load(board)
|
|
2455
|
+
} catch (e) {
|
|
2456
|
+
setError(t(e.key || e.error || 'nudge.failed'))
|
|
2457
|
+
} finally {
|
|
2458
|
+
setNudgeSending(false)
|
|
2459
|
+
}
|
|
2460
|
+
}, [nudgeTask, nudgePrompt, nudgeSending, load, board, t])
|
|
2320
2461
|
|
|
2321
2462
|
const load = React.useCallback(async (nextBoard) => {
|
|
2322
2463
|
try {
|
|
@@ -2868,6 +3009,7 @@ window.__ModuleLoader__.load({
|
|
|
2868
3009
|
if (manyGroups && groups[col.id + '/' + (task.repo || '')] === true) return
|
|
2869
3010
|
rows.push(h(TaskCard, {
|
|
2870
3011
|
key: task.id, task, t, now: state.now,
|
|
3012
|
+
onNudge: (t) => { setNudgeTask(t); setNudgePrompt(''); setNudgeSuccess(false) },
|
|
2871
3013
|
columns: (state.columns || []).map((c) => c.id),
|
|
2872
3014
|
menuOpen: cardMenu,
|
|
2873
3015
|
onMenu: setCardMenu,
|
|
@@ -3328,6 +3470,59 @@ window.__ModuleLoader__.load({
|
|
|
3328
3470
|
},
|
|
3329
3471
|
}, t('panel.openChat'))
|
|
3330
3472
|
: null,
|
|
3473
|
+
openTask.sessionId && (openTask.column === 'in-progress' || openTask.col === 'in-progress')
|
|
3474
|
+
? h('button', {
|
|
3475
|
+
type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
|
|
3476
|
+
onClick: () => { setNudgeTask(openTask); setNudgePrompt(''); setNudgeSuccess(false) },
|
|
3477
|
+
}, '💬 ' + t('card.nudge'))
|
|
3478
|
+
: null,
|
|
3479
|
+
openTask.costMetrics && openTask.costMetrics.totalTokens > 0
|
|
3480
|
+
? h('div', {
|
|
3481
|
+
className: 'dkb-section-card',
|
|
3482
|
+
style: {
|
|
3483
|
+
margin: '8px 0', padding: 12, border: '1px solid var(--dsw-alias-border-l2)',
|
|
3484
|
+
borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 6,
|
|
3485
|
+
},
|
|
3486
|
+
},
|
|
3487
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3488
|
+
h('div', { className: 'dkb-group', style: { margin: 0, fontWeight: 600 } },
|
|
3489
|
+
'🪙 ' + t('telemetry.title')
|
|
3490
|
+
),
|
|
3491
|
+
h('span', {
|
|
3492
|
+
className: 'dkb-tag',
|
|
3493
|
+
style: { background: 'var(--dsw-alias-state-business-tertiary, #ddf4ff)', color: 'var(--dsw-alias-state-business-primary, #0969da)', borderColor: 'var(--dsw-alias-state-business-tertiary, #54aeff)' }
|
|
3494
|
+
}, openTask.costMetrics.costFormatted || '$0.00'),
|
|
3495
|
+
),
|
|
3496
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, fontSize: '11px', textAlign: 'center' } },
|
|
3497
|
+
h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
|
|
3498
|
+
h('div', { style: { opacity: 0.7 } }, t('telemetry.tokens')),
|
|
3499
|
+
h('div', { style: { fontWeight: 600 } }, openTask.costMetrics.totalTokens.toLocaleString()),
|
|
3500
|
+
),
|
|
3501
|
+
h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
|
|
3502
|
+
h('div', { style: { opacity: 0.7 } }, t('telemetry.input')),
|
|
3503
|
+
h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.uncachedInputTokens || 0).toLocaleString()),
|
|
3504
|
+
),
|
|
3505
|
+
h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
|
|
3506
|
+
h('div', { style: { opacity: 0.7 } }, t('telemetry.output')),
|
|
3507
|
+
h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.outputTokens || 0).toLocaleString()),
|
|
3508
|
+
),
|
|
3509
|
+
h('div', { style: { background: 'var(--dsw-alias-bg-layer-1)', padding: '6px', borderRadius: '4px' } },
|
|
3510
|
+
h('div', { style: { opacity: 0.7 } }, t('telemetry.cache')),
|
|
3511
|
+
h('div', { style: { fontWeight: 600 } }, (openTask.costMetrics.cacheReadTokens || 0).toLocaleString()),
|
|
3512
|
+
),
|
|
3513
|
+
),
|
|
3514
|
+
openTask.costLimitExceeded
|
|
3515
|
+
? h('div', {
|
|
3516
|
+
style: {
|
|
3517
|
+
fontSize: '11px',
|
|
3518
|
+
color: 'var(--dsw-alias-state-warn-primary, #9a6700)',
|
|
3519
|
+
background: 'var(--dsw-alias-state-warn-secondary, #fff8c5)',
|
|
3520
|
+
padding: '4px 8px', borderRadius: '4px', marginTop: '4px'
|
|
3521
|
+
}
|
|
3522
|
+
}, '⚠️ ' + t('card.costLimitExceeded'))
|
|
3523
|
+
: null
|
|
3524
|
+
)
|
|
3525
|
+
: null,
|
|
3331
3526
|
log.length
|
|
3332
3527
|
? h('div', null,
|
|
3333
3528
|
h('div', { className: 'dkb-group' }, t('panel.log')),
|
|
@@ -4372,11 +4567,76 @@ window.__ModuleLoader__.load({
|
|
|
4372
4567
|
)
|
|
4373
4568
|
: null
|
|
4374
4569
|
|
|
4570
|
+
const nudgeDialogEl = nudgeTask
|
|
4571
|
+
? h('div', {
|
|
4572
|
+
className: 'dkb-modal',
|
|
4573
|
+
onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
|
|
4574
|
+
},
|
|
4575
|
+
h('div', {
|
|
4576
|
+
className: 'dkb-dialog',
|
|
4577
|
+
style: { maxWidth: '440px', width: '90%', padding: '16px' },
|
|
4578
|
+
onClick: (e) => e.stopPropagation(),
|
|
4579
|
+
},
|
|
4580
|
+
h('div', { className: 'dkb-dialogHead', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' } },
|
|
4581
|
+
h('h3', { style: { margin: 0, fontSize: '15px' } }, t('nudge.title') + ': ' + (nudgeTask.title || '')),
|
|
4582
|
+
h('button', {
|
|
4583
|
+
type: 'button',
|
|
4584
|
+
className: 'dkb-discard',
|
|
4585
|
+
style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '16px', padding: '0 4px' },
|
|
4586
|
+
onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
|
|
4587
|
+
}, '✕'),
|
|
4588
|
+
),
|
|
4589
|
+
nudgeSuccess
|
|
4590
|
+
? h('div', {
|
|
4591
|
+
style: {
|
|
4592
|
+
padding: '12px',
|
|
4593
|
+
borderRadius: '6px',
|
|
4594
|
+
background: 'var(--dsw-alias-state-success-secondary, #dafbe1)',
|
|
4595
|
+
color: 'var(--dsw-alias-state-success-primary, #1a7f37)',
|
|
4596
|
+
marginBottom: '12px',
|
|
4597
|
+
textAlign: 'center',
|
|
4598
|
+
fontWeight: '600',
|
|
4599
|
+
},
|
|
4600
|
+
}, '✓ ' + t('nudge.success'))
|
|
4601
|
+
: h('div', null,
|
|
4602
|
+
h('textarea', {
|
|
4603
|
+
className: 'dkb-area',
|
|
4604
|
+
style: { width: '100%', minHeight: '90px', marginBottom: '12px', boxSizing: 'border-box' },
|
|
4605
|
+
placeholder: t('nudge.placeholder'),
|
|
4606
|
+
value: nudgePrompt,
|
|
4607
|
+
autoFocus: true,
|
|
4608
|
+
onChange: (e) => setNudgePrompt(e.target.value),
|
|
4609
|
+
onKeyDown: (e) => {
|
|
4610
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
4611
|
+
e.preventDefault();
|
|
4612
|
+
handleNudgeSubmit();
|
|
4613
|
+
}
|
|
4614
|
+
},
|
|
4615
|
+
}),
|
|
4616
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px' } },
|
|
4617
|
+
h('button', {
|
|
4618
|
+
type: 'button',
|
|
4619
|
+
className: 'dkb-discard',
|
|
4620
|
+
disabled: nudgeSending,
|
|
4621
|
+
onClick: () => { setNudgeTask(null); setNudgePrompt(''); setNudgeSuccess(false) },
|
|
4622
|
+
}, t('panel.close')),
|
|
4623
|
+
h('button', {
|
|
4624
|
+
type: 'button',
|
|
4625
|
+
className: 'dkb-save',
|
|
4626
|
+
disabled: !nudgePrompt.trim() || nudgeSending,
|
|
4627
|
+
onClick: handleNudgeSubmit,
|
|
4628
|
+
}, nudgeSending ? t('nudge.sending') : t('nudge.send')),
|
|
4629
|
+
),
|
|
4630
|
+
),
|
|
4631
|
+
),
|
|
4632
|
+
)
|
|
4633
|
+
: null;
|
|
4634
|
+
|
|
4375
4635
|
return h('div', { className: 'dkb-screen' },
|
|
4376
4636
|
style, bar,
|
|
4377
4637
|
error ? h('p', { className: 'dkb-failed' }, error) : null,
|
|
4378
4638
|
metrics ? metricsEl : archive ? archiveEl : columns,
|
|
4379
|
-
panel, dialogEl, moveDialogEl, batchEl, queueEl,
|
|
4639
|
+
panel, dialogEl, moveDialogEl, batchEl, queueEl, nudgeDialogEl,
|
|
4380
4640
|
)
|
|
4381
4641
|
}
|
|
4382
4642
|
|
package/lib/config.js
CHANGED
|
@@ -79,6 +79,8 @@ export const CONFIG_DEFAULTS = {
|
|
|
79
79
|
verificationGateEnabled: true,
|
|
80
80
|
verificationCommand: 'npm test',
|
|
81
81
|
verificationTimeoutMs: 120000,
|
|
82
|
+
costMeterEnabled: true,
|
|
83
|
+
taskCostLimit: 0,
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/** Field descriptions reused in schema and settings card. */
|
|
@@ -117,6 +119,8 @@ export const CONFIG_HINTS = {
|
|
|
117
119
|
verificationGateEnabled: 'Run automated verification gate (e.g. npm test) before allowing cards to move to Review.',
|
|
118
120
|
verificationCommand: 'Default verification command to execute in task worktree before Review (default: npm test).',
|
|
119
121
|
verificationTimeoutMs: 'Timeout in milliseconds for verification gate execution (default: 120000).',
|
|
122
|
+
costMeterEnabled: 'Enable token and cost telemetry integration via @goodandready/dsh-cost-meter.',
|
|
123
|
+
taskCostLimit: 'Cost threshold per task in USD; 0 disables threshold warnings.',
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/** Setting key for column WIP limit; not all columns have limits. */
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token and cost telemetry integration with @goodandready/dsh-cost-meter (#374).
|
|
3
|
+
* All functions fail-soft without throwing when cost-meter is absent.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check whether @goodandready/dsh-cost-meter plugin is active in current runtime.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} ctx - Cordis context
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isCostMeterAvailable(ctx) {
|
|
13
|
+
if (!ctx || typeof ctx !== 'object') return false
|
|
14
|
+
try {
|
|
15
|
+
if (ctx.costMeter || (typeof ctx.get === 'function' && ctx.get('costMeter'))) return true
|
|
16
|
+
if (ctx.settings && typeof ctx.settings.describe === 'function') {
|
|
17
|
+
const rows = ctx.settings.describe({ redactSecrets: true })
|
|
18
|
+
if (Array.isArray(rows) && rows.some((r) => r?.ns === 'dsh-cost-meter')) return true
|
|
19
|
+
}
|
|
20
|
+
if (ctx.registry && typeof ctx.registry[Symbol.iterator] === 'function') {
|
|
21
|
+
for (const [plugin] of ctx.registry) {
|
|
22
|
+
if (plugin === '@goodandready/dsh-cost-meter' || plugin?.name === '@goodandready/dsh-cost-meter') {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
} catch {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract aggregated token usage from a session's costByModel projection view.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} projectionView - View object from usageByModelProjection
|
|
37
|
+
* @returns {{
|
|
38
|
+
* totalTokens: number,
|
|
39
|
+
* uncachedInputTokens: number,
|
|
40
|
+
* outputTokens: number,
|
|
41
|
+
* cacheReadTokens: number,
|
|
42
|
+
* cacheWriteTokens: number,
|
|
43
|
+
* turnsCount: number,
|
|
44
|
+
* models: Array<string>
|
|
45
|
+
* }}
|
|
46
|
+
*/
|
|
47
|
+
export function extractSessionUsage(projectionView) {
|
|
48
|
+
const result = {
|
|
49
|
+
totalTokens: 0,
|
|
50
|
+
uncachedInputTokens: 0,
|
|
51
|
+
outputTokens: 0,
|
|
52
|
+
cacheReadTokens: 0,
|
|
53
|
+
cacheWriteTokens: 0,
|
|
54
|
+
turnsCount: 0,
|
|
55
|
+
models: [],
|
|
56
|
+
}
|
|
57
|
+
if (!projectionView || typeof projectionView !== 'object') return result
|
|
58
|
+
|
|
59
|
+
const modelsList = Array.isArray(projectionView.models) ? projectionView.models : []
|
|
60
|
+
for (const m of modelsList) {
|
|
61
|
+
const key = (m.provider ? m.provider + '/' : '') + (m.model || '')
|
|
62
|
+
if (key && !result.models.includes(key)) result.models.push(key)
|
|
63
|
+
const slots = m.slots && typeof m.slots === 'object' ? m.slots : {}
|
|
64
|
+
for (const slotKey of Object.keys(slots)) {
|
|
65
|
+
const b = slots[slotKey] || {}
|
|
66
|
+
result.uncachedInputTokens += Number(b.uncachedInputTokens) || 0
|
|
67
|
+
result.outputTokens += Number(b.outputTokens) || 0
|
|
68
|
+
result.cacheReadTokens += Number(b.cacheReadTokens) || 0
|
|
69
|
+
result.cacheWriteTokens += Number(b.cacheWriteTokens) || 0
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(projectionView.turns)) {
|
|
74
|
+
result.turnsCount = projectionView.turns.length
|
|
75
|
+
if (result.totalTokens === 0 && projectionView.turns.length > 0) {
|
|
76
|
+
for (const t of projectionView.turns) {
|
|
77
|
+
result.uncachedInputTokens += Number(t.uncachedInputTokens) || 0
|
|
78
|
+
result.outputTokens += Number(t.outputTokens) || 0
|
|
79
|
+
result.cacheReadTokens += Number(t.cacheReadTokens) || 0
|
|
80
|
+
result.cacheWriteTokens += Number(t.cacheWriteTokens) || 0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
result.totalTokens = result.uncachedInputTokens + result.outputTokens + result.cacheReadTokens + result.cacheWriteTokens
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Calculate cost for token usage given rate schedule or fallback tariff.
|
|
91
|
+
* Default tariff: standard DeepSeek rates ($0.14/M input, $0.28/M output, $0.014/M cache hit).
|
|
92
|
+
*
|
|
93
|
+
* @param {{
|
|
94
|
+
* usage: object,
|
|
95
|
+
* schedule?: object,
|
|
96
|
+
* usdRate?: number,
|
|
97
|
+
* currency?: string
|
|
98
|
+
* }} options
|
|
99
|
+
* @returns {{
|
|
100
|
+
* costUsd: number,
|
|
101
|
+
* costFormatted: string,
|
|
102
|
+
* currency: string
|
|
103
|
+
* }}
|
|
104
|
+
*/
|
|
105
|
+
export function calculateTokenCost({ usage, schedule, usdRate = 1, currency = '$' }) {
|
|
106
|
+
if (!usage || typeof usage !== 'object') {
|
|
107
|
+
return { costUsd: 0, costFormatted: currency + '0.00', currency }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const uncachedInput = Number(usage.uncachedInputTokens) || 0
|
|
111
|
+
const output = Number(usage.outputTokens) || 0
|
|
112
|
+
const cacheRead = Number(usage.cacheReadTokens) || 0
|
|
113
|
+
const cacheWrite = Number(usage.cacheWriteTokens) || 0
|
|
114
|
+
|
|
115
|
+
const rateInput = Number(schedule?.inputPerMillion ?? schedule?.input) || 0.14
|
|
116
|
+
const rateOutput = Number(schedule?.outputPerMillion ?? schedule?.output) || 0.28
|
|
117
|
+
const rateCacheRead = Number(schedule?.cacheReadPerMillion ?? schedule?.cacheHit) || 0.014
|
|
118
|
+
const rateCacheWrite = Number(schedule?.cacheWritePerMillion) || rateInput
|
|
119
|
+
|
|
120
|
+
const costUsd = (uncachedInput * rateInput + output * rateOutput + cacheRead * rateCacheRead + cacheWrite * rateCacheWrite) / 1_000_000
|
|
121
|
+
const converted = costUsd * (Number(usdRate) > 0 ? Number(usdRate) : 1)
|
|
122
|
+
|
|
123
|
+
const costFormatted = currency + (converted < 0.01 && converted > 0 ? '<0.01' : converted.toFixed(2))
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
costUsd: Math.round(costUsd * 10000) / 10000,
|
|
127
|
+
costFormatted,
|
|
128
|
+
currency,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check whether task cost exceeds configured budget limit.
|
|
134
|
+
*
|
|
135
|
+
* @param {{ costUsd: number, limit: number }} options
|
|
136
|
+
* @returns {{ exceeded: boolean, limit: number, costUsd: number }}
|
|
137
|
+
*/
|
|
138
|
+
export function checkCostLimit({ costUsd = 0, limit = 0 }) {
|
|
139
|
+
const numLimit = Number(limit) || 0
|
|
140
|
+
if (numLimit <= 0) return { exceeded: false, limit: 0, costUsd }
|
|
141
|
+
return {
|
|
142
|
+
exceeded: costUsd >= numLimit,
|
|
143
|
+
limit: numLimit,
|
|
144
|
+
costUsd,
|
|
145
|
+
}
|
|
146
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -51,7 +51,9 @@ import {
|
|
|
51
51
|
handleCreateSubtask,
|
|
52
52
|
handleToggleChecklistItem,
|
|
53
53
|
handleCreateTaskPr,
|
|
54
|
+
handleTaskNudge,
|
|
54
55
|
} from './routes.js'
|
|
56
|
+
import { isCostMeterAvailable } from './cost-telemetry.js'
|
|
55
57
|
import { listImportable, importIssue, refreshTask, searchRepos, createProjectTask } from './import.js'
|
|
56
58
|
import {
|
|
57
59
|
runTask, resolveModel, buildStartMessage,
|
|
@@ -867,7 +869,20 @@ export function apply(ctx, config) {
|
|
|
867
869
|
/**
|
|
868
870
|
* Return active agent state or undefined if session is inactive.
|
|
869
871
|
*/
|
|
870
|
-
const liveOf = (sessionId) =>
|
|
872
|
+
const liveOf = (sessionId) => {
|
|
873
|
+
try {
|
|
874
|
+
const a = ctx.agents?.get?.(sessionId);
|
|
875
|
+
if (!a) return undefined;
|
|
876
|
+
return {
|
|
877
|
+
status: a.status || (typeof a.isIdle === 'function' && a.isIdle() ? 'idle' : 'running'),
|
|
878
|
+
state: a.state || a.lifecycleState,
|
|
879
|
+
step: a.step || a.currentStep || a.stateDetail || a.activity || '',
|
|
880
|
+
activity: a.activity || a.statusDetail || '',
|
|
881
|
+
};
|
|
882
|
+
} catch {
|
|
883
|
+
return undefined;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
871
886
|
|
|
872
887
|
ctx.effect(() => ctx.webServer.register({
|
|
873
888
|
kind: 'exact',
|
|
@@ -885,6 +900,7 @@ export function apply(ctx, config) {
|
|
|
885
900
|
sync: syncState.snapshot(),
|
|
886
901
|
// Resolved project root passed with board payload for session launch modal.
|
|
887
902
|
projectRoot: rootOf(getConfig()),
|
|
903
|
+
costMeterAvailable: isCostMeterAvailable(ctx),
|
|
888
904
|
}))
|
|
889
905
|
},
|
|
890
906
|
}), 'dsh-kanban: board route')
|
|
@@ -1230,6 +1246,18 @@ export function apply(ctx, config) {
|
|
|
1230
1246
|
writeJson(res, resObj.status, resObj.body)
|
|
1231
1247
|
return
|
|
1232
1248
|
}
|
|
1249
|
+
if (req.method === 'POST' && (action === 'nudge' || (input && input.action === 'nudge'))) {
|
|
1250
|
+
const resObj = await handleTaskNudge({
|
|
1251
|
+
store,
|
|
1252
|
+
id,
|
|
1253
|
+
prompt: input?.prompt || input?.text,
|
|
1254
|
+
agents: ctx.agents,
|
|
1255
|
+
createMessage: createUserMessage,
|
|
1256
|
+
eventHub,
|
|
1257
|
+
})
|
|
1258
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1259
|
+
return
|
|
1260
|
+
}
|
|
1233
1261
|
if (req.method === 'GET' && action === 'diff') {
|
|
1234
1262
|
const resObj = await handleTaskDiff({ task: store.getTask(id), getTaskDiff, getTaskDiffInspector })
|
|
1235
1263
|
writeJson(res, resObj.status, resObj.body)
|
package/lib/routes.js
CHANGED
|
@@ -308,10 +308,23 @@ function withPlan(task, liveOf, config, now, sameSession) {
|
|
|
308
308
|
try {
|
|
309
309
|
if (typeof liveOf === 'function' && task.sessionId) live = liveOf(task.sessionId)
|
|
310
310
|
} catch { live = undefined }
|
|
311
|
+
const liveStatus = typeof live === 'object' && live !== null ? live.status : live
|
|
312
|
+
const liveStep = typeof live === 'object' && live !== null
|
|
313
|
+
? (live.step || live.activity || live.stateDetail || '')
|
|
314
|
+
: ''
|
|
315
|
+
|
|
316
|
+
const costMetrics = task.costMetrics || null
|
|
317
|
+
const costLimitExceeded = (config?.costMeterEnabled !== false && Number(config?.taskCostLimit) > 0 && costMetrics?.costUsd)
|
|
318
|
+
? costMetrics.costUsd >= Number(config.taskCostLimit)
|
|
319
|
+
: Boolean(task.costLimitExceeded)
|
|
320
|
+
|
|
311
321
|
return {
|
|
312
322
|
...task,
|
|
313
323
|
plan: { items: planItems(plan), progress: planProgress(plan) },
|
|
314
|
-
state: taskState({ task, live }),
|
|
324
|
+
state: taskState({ task, live: liveStatus }),
|
|
325
|
+
liveStep: liveStep || task.liveStep || '',
|
|
326
|
+
costMetrics,
|
|
327
|
+
costLimitExceeded,
|
|
315
328
|
// Queue position calculated dynamically across active tasks in same session.
|
|
316
329
|
queuePos: queuePosition(task, sameSession),
|
|
317
330
|
// Label facets extracted on server for consistent client filtering.
|
|
@@ -344,7 +357,7 @@ function orderForBoard(tasks, columns) {
|
|
|
344
357
|
/**
|
|
345
358
|
* Build board response payload: columns in canonical order, tasks, and limits.
|
|
346
359
|
*/
|
|
347
|
-
export function buildBoard({ store, config, board = 'main', repo, liveOf, sync, projectRoot }) {
|
|
360
|
+
export function buildBoard({ store, config, board = 'main', repo, liveOf, sync, projectRoot, costMeterAvailable = false }) {
|
|
348
361
|
// Uniform timestamp across snapshot calculation.
|
|
349
362
|
const now = Date.now()
|
|
350
363
|
// Tasks are sorted and grouped on server: repo grouping with active items first.
|
|
@@ -384,6 +397,7 @@ export function buildBoard({ store, config, board = 'main', repo, liveOf, sync,
|
|
|
384
397
|
facets: facetsOf(tasks),
|
|
385
398
|
// Sync telemetry state attached directly to board snapshot.
|
|
386
399
|
sync: sync ?? { state: 'never' },
|
|
400
|
+
costMeterAvailable: Boolean(costMeterAvailable),
|
|
387
401
|
}
|
|
388
402
|
}
|
|
389
403
|
|
|
@@ -1119,4 +1133,75 @@ export async function handleCreateTaskPr({ store, gitea, task, input }) {
|
|
|
1119
1133
|
} catch (err) {
|
|
1120
1134
|
return { status: 502, body: { error: 'gitea-pr-failed', detail: err?.message } }
|
|
1121
1135
|
}
|
|
1122
|
-
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Handle interactive live steering (nudge) for an active task agent (#373).
|
|
1139
|
+
*
|
|
1140
|
+
* @param {{
|
|
1141
|
+
* store: object,
|
|
1142
|
+
* id: string,
|
|
1143
|
+
* prompt: string,
|
|
1144
|
+
* agents?: object,
|
|
1145
|
+
* createMessage?: Function,
|
|
1146
|
+
* eventHub?: object,
|
|
1147
|
+
* now?: number
|
|
1148
|
+
* }} options
|
|
1149
|
+
* @returns {Promise<{ status: number, body: object }>}
|
|
1150
|
+
*/
|
|
1151
|
+
export async function handleTaskNudge({ store, id, prompt, agents, createMessage, eventHub, now = Date.now() }) {
|
|
1152
|
+
if (!store || !id) return { status: 400, body: { error: 'invalid-parameters' } }
|
|
1153
|
+
const task = store.getTask(id)
|
|
1154
|
+
if (!task) return { status: 404, body: { error: 'task-not-found' } }
|
|
1155
|
+
|
|
1156
|
+
const text = typeof prompt === 'string' ? prompt.trim() : ''
|
|
1157
|
+
if (!text) return { status: 400, body: { error: 'prompt-required' } }
|
|
1158
|
+
|
|
1159
|
+
const sessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
|
|
1160
|
+
if (!sessionId) return { status: 409, body: { error: 'no-session' } }
|
|
1161
|
+
|
|
1162
|
+
let agent
|
|
1163
|
+
try { agent = agents?.get?.(sessionId) } catch { agent = undefined }
|
|
1164
|
+
if (!agent || typeof agent.followup !== 'function') {
|
|
1165
|
+
return { status: 409, body: { error: 'session-not-live' } }
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const msg = typeof createMessage === 'function'
|
|
1169
|
+
? createMessage({
|
|
1170
|
+
content: [{ type: 'text', text }],
|
|
1171
|
+
source: { kind: 'dsh-kanban', plugin: 'dsh-kanban', form: 'task-nudge' },
|
|
1172
|
+
})
|
|
1173
|
+
: { content: [{ type: 'text', text }] }
|
|
1174
|
+
|
|
1175
|
+
agent.followup(msg)
|
|
1176
|
+
|
|
1177
|
+
// Append user steering comment to task
|
|
1178
|
+
if (typeof store.addComment === 'function') {
|
|
1179
|
+
store.addComment(id, {
|
|
1180
|
+
author: 'user',
|
|
1181
|
+
role: 'user',
|
|
1182
|
+
text: `[Steering Nudge] ${text}`,
|
|
1183
|
+
})
|
|
1184
|
+
} else if (typeof store.addTaskComment === 'function') {
|
|
1185
|
+
store.addTaskComment(id, {
|
|
1186
|
+
author: 'user',
|
|
1187
|
+
role: 'user',
|
|
1188
|
+
text: `[Steering Nudge] ${text}`,
|
|
1189
|
+
})
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Broadcast change via eventHub
|
|
1193
|
+
if (eventHub && typeof eventHub.broadcast === 'function') {
|
|
1194
|
+
eventHub.broadcast('task:change', { id, col: task.col || task.column })
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return {
|
|
1198
|
+
status: 200,
|
|
1199
|
+
body: {
|
|
1200
|
+
success: true,
|
|
1201
|
+
id,
|
|
1202
|
+
sessionId,
|
|
1203
|
+
prompt: text,
|
|
1204
|
+
nudgedAt: now,
|
|
1205
|
+
},
|
|
1206
|
+
}
|
|
1207
|
+
}
|
package/lib/store.js
CHANGED
|
@@ -109,6 +109,7 @@ const WRITABLE = [
|
|
|
109
109
|
'runs', 'checklist', 'report', 'comments', 'rejectReason', 'rejectedAt', 'parentId', 'isEpic',
|
|
110
110
|
'workflowRunId', 'workflowStatus', 'permissionConfirmedTier', 'forge', 'queuedRequest',
|
|
111
111
|
'verificationCommand', 'verificationStatus',
|
|
112
|
+
'costMetrics', 'costLimitExceeded',
|
|
112
113
|
]
|
|
113
114
|
|
|
114
115
|
/** Valid transition sources; unexpected values are rejected to preserve log integrity. */
|
|
@@ -162,6 +163,16 @@ function rowToTask(row) {
|
|
|
162
163
|
reasoningEffort: rest.reasoningEffort ?? '',
|
|
163
164
|
isEpic: isEpic === 1,
|
|
164
165
|
parentId: parentId ?? '',
|
|
166
|
+
costMetrics: (() => {
|
|
167
|
+
if (typeof rest.costMetrics === 'string' && rest.costMetrics.trim()) {
|
|
168
|
+
try {
|
|
169
|
+
const val = JSON.parse(rest.costMetrics);
|
|
170
|
+
if (val && typeof val === 'object') return val;
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
return rest.costMetrics && typeof rest.costMetrics === 'object' ? rest.costMetrics : null;
|
|
174
|
+
})(),
|
|
175
|
+
costLimitExceeded: rest.costLimitExceeded === 1 || rest.costLimitExceeded === true,
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
|
|
@@ -252,6 +263,8 @@ export function openStore({ dir }) {
|
|
|
252
263
|
['queuedRequest', "TEXT NOT NULL DEFAULT ''"],
|
|
253
264
|
['verificationCommand', "TEXT NOT NULL DEFAULT ''"],
|
|
254
265
|
['verificationStatus', "TEXT NOT NULL DEFAULT ''"],
|
|
266
|
+
['costMetrics', "TEXT NOT NULL DEFAULT ''"],
|
|
267
|
+
['costLimitExceeded', "INTEGER NOT NULL DEFAULT 0"],
|
|
255
268
|
]) {
|
|
256
269
|
try { db.exec(`ALTER TABLE tasks ADD COLUMN ${column} ${definition}`) } catch { /* column already exists */ }
|
|
257
270
|
}
|
|
@@ -653,6 +666,15 @@ export function openStore({ dir }) {
|
|
|
653
666
|
})
|
|
654
667
|
},
|
|
655
668
|
|
|
669
|
+
appendComment(id, comment) {
|
|
670
|
+
if (!comment || typeof comment !== 'object') return undefined
|
|
671
|
+
return this.addComment(id, {
|
|
672
|
+
author: comment.author || 'user',
|
|
673
|
+
role: comment.role || 'user',
|
|
674
|
+
text: comment.text || comment.body || '',
|
|
675
|
+
})
|
|
676
|
+
},
|
|
677
|
+
|
|
656
678
|
updateChecklist(id, items) {
|
|
657
679
|
const task = rowToTask(q.taskById.get(id))
|
|
658
680
|
if (!task) return undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
4
4
|
"description": "Visual Kanban board for DeepSeek Harness: Gitea and GitHub-backed tasks, workflow columns, and a dedicated agent session per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|