@goodandready/dsh-kanban 0.2.13 → 0.2.14
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 +93 -23
- package/lib/commands.js +1 -0
- package/lib/gitea.js +3 -1
- package/lib/index.js +25 -8
- package/lib/launcher.js +1 -1
- package/lib/markdown.js +1 -1
- package/lib/routes.js +44 -30
- package/lib/store.js +22 -7
- package/lib/time.js +2 -1
- package/lib/worktree.js +6 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -608,6 +608,7 @@ window.__ModuleLoader__.load({
|
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
const helpers = {
|
|
611
|
+
ROW_ID, ROW_CONFIG_KEY, PKG, KanbanSettingsCard,
|
|
611
612
|
neighboursFor, tasksOf, sortTasks, packInfo, groupByAssignee, applyToEach, recallLaunch, rememberLaunch, stillOffered,
|
|
612
613
|
cardRef, taskRef, cardStatus, facetLabel, boardTitle,
|
|
613
614
|
columnTitle, planLine, stateLabel, isCollapsed,
|
|
@@ -721,6 +722,8 @@ window.__ModuleLoader__.load({
|
|
|
721
722
|
'.dkb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:8px;margin-bottom:12px}' +'.dkb-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px;padding-bottom:4px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +'.dkb-section-badge{font-size:11px;font-weight:500;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}' +'.dkb-section-badge-ok{border-color:var(--dsw-alias-state-success-border,#34d399);color:var(--dsw-alias-state-success-primary,#059669);background:var(--dsw-alias-state-success-bg,rgba(16,185,129,0.1))}' +
|
|
722
723
|
'.dkb-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
723
724
|
'.dkb-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
725
|
+
'.dkb-page{padding:0}' +
|
|
726
|
+
'.dkb-page .dkb-body{border-top:none;margin:0}' +
|
|
724
727
|
'.dkb-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
725
728
|
'.dkb-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
726
729
|
'.dkb-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
@@ -1818,8 +1821,16 @@ window.__ModuleLoader__.load({
|
|
|
1818
1821
|
}
|
|
1819
1822
|
|
|
1820
1823
|
function KanbanSettingsCard(props) {
|
|
1821
|
-
const ctx = props.ctx
|
|
1822
|
-
const t = props.t || fallbackT
|
|
1824
|
+
const ctx = props && props.ctx
|
|
1825
|
+
const t = (props && props.t) || fallbackT
|
|
1826
|
+
ensureCss()
|
|
1827
|
+
|
|
1828
|
+
// Row seat (plugins.row.config): summary is a one-liner
|
|
1829
|
+
if (props && props.view === 'summary') {
|
|
1830
|
+
return h('span', { className: 'dkb-sub' }, t('settings.subtitle') || t('subtitle') || 'Kanban board for sessions and tasks')
|
|
1831
|
+
}
|
|
1832
|
+
const page = !!(props && props.view === 'page')
|
|
1833
|
+
|
|
1823
1834
|
const [open, setOpen] = React.useState(false)
|
|
1824
1835
|
const [draft, setDraft] = React.useState(null)
|
|
1825
1836
|
const [saving, setSaving] = React.useState(false)
|
|
@@ -1891,7 +1902,7 @@ window.__ModuleLoader__.load({
|
|
|
1891
1902
|
ensureCss()
|
|
1892
1903
|
const style = null
|
|
1893
1904
|
|
|
1894
|
-
if (!open) return h('li', { className: 'dkb-card' }, head)
|
|
1905
|
+
if (!page && !open) return h('li', { className: 'dkb-card' }, head)
|
|
1895
1906
|
|
|
1896
1907
|
let body
|
|
1897
1908
|
if (status === 'loading') {
|
|
@@ -1980,15 +1991,10 @@ window.__ModuleLoader__.load({
|
|
|
1980
1991
|
body = rows
|
|
1981
1992
|
}
|
|
1982
1993
|
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
if (props && props.view === 'summary') {
|
|
1986
|
-
return h('span', { className: 'dkb-sub' }, t('settings.subtitle') || 'Kanban board for sessions and tasks')
|
|
1994
|
+
if (page) {
|
|
1995
|
+
return h('div', { className: 'dkb-page' }, h('div', { className: 'dkb-body' }, body))
|
|
1987
1996
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
return h(page ? 'div' : 'li', { className: page ? 'dkb-page' : 'dkb-card dkb-cardOpen' },
|
|
1991
|
-
page ? null : head, h('div', { className: 'dkb-body' }, body))
|
|
1997
|
+
return h('li', { className: 'dkb-card dkb-cardOpen' }, head, h('div', { className: 'dkb-body' }, body))
|
|
1992
1998
|
}
|
|
1993
1999
|
|
|
1994
2000
|
/** Стрелка влево для кнопки возврата: 14x14, обводка цветом текста. */
|
|
@@ -2238,6 +2244,36 @@ window.__ModuleLoader__.load({
|
|
|
2238
2244
|
const [syncing, setSyncing] = React.useState(false)
|
|
2239
2245
|
const [picked, setPicked] = React.useState({})
|
|
2240
2246
|
const [archive, setArchive] = React.useState(null)
|
|
2247
|
+
const [diffExpanded, setDiffExpanded] = React.useState(false)
|
|
2248
|
+
const [diffLoading, setDiffLoading] = React.useState(false)
|
|
2249
|
+
const [diffData, setDiffData] = React.useState(null)
|
|
2250
|
+
const [commitsData, setCommitsData] = React.useState([])
|
|
2251
|
+
const [merging, setMerging] = React.useState(false)
|
|
2252
|
+
|
|
2253
|
+
const loadDiffAndCommits = React.useCallback(async () => {
|
|
2254
|
+
if (!openTask) return
|
|
2255
|
+
setDiffLoading(true)
|
|
2256
|
+
try {
|
|
2257
|
+
const [dRes, cRes] = await Promise.all([
|
|
2258
|
+
api('/task/' + encodeURIComponent(openTask.id) + '/diff'),
|
|
2259
|
+
api('/task/' + encodeURIComponent(openTask.id) + '/commits'),
|
|
2260
|
+
])
|
|
2261
|
+
setDiffData(dRes)
|
|
2262
|
+
setCommitsData(cRes?.commits || [])
|
|
2263
|
+
} catch (e) {
|
|
2264
|
+
setError(t(e.key || e.error || 'error.unknown'))
|
|
2265
|
+
} finally {
|
|
2266
|
+
setDiffLoading(false)
|
|
2267
|
+
}
|
|
2268
|
+
}, [openTask, t])
|
|
2269
|
+
|
|
2270
|
+
React.useEffect(() => {
|
|
2271
|
+
setDiffExpanded(false)
|
|
2272
|
+
setDiffLoading(false)
|
|
2273
|
+
setDiffData(null)
|
|
2274
|
+
setCommitsData([])
|
|
2275
|
+
setMerging(false)
|
|
2276
|
+
}, [openTask?.id])
|
|
2241
2277
|
const [metrics, setMetrics] = React.useState(null)
|
|
2242
2278
|
const [onlyWaiting, setOnlyWaiting] = React.useState(false)
|
|
2243
2279
|
const [onlyOverdue, setOnlyOverdue] = React.useState(false)
|
|
@@ -4518,25 +4554,59 @@ window.__ModuleLoader__.load({
|
|
|
4518
4554
|
// below — the seat is additive, not an either/or candidate. The label is a static
|
|
4519
4555
|
// string on purpose: it is resolved while the page renders, and a locale lookup
|
|
4520
4556
|
// there would take the whole client batch down with it.
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
ctx.slots.register
|
|
4557
|
+
const trySlot = (name, register) => {
|
|
4558
|
+
try {
|
|
4559
|
+
if (typeof ctx.slots?.inject === 'function') ctx.slots.inject(name, register)
|
|
4560
|
+
else register()
|
|
4561
|
+
} catch (e) {
|
|
4562
|
+
console.warn('[dsh-kanban] slot registration failed for ' + name + ':', e && e.message)
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
// #274: Newest slot first, older slots as fallbacks:
|
|
4567
|
+
// 1. plugins.row.config (Row seat on Plugins page, DSH 0.1.6-alpha.2)
|
|
4568
|
+
let cardSlot = undefined
|
|
4569
|
+
trySlot('plugins.row.config', () => {
|
|
4570
|
+
ctx.slots.register(
|
|
4571
|
+
{
|
|
4572
|
+
name: 'plugins.row.config',
|
|
4573
|
+
key: ROW_CONFIG_KEY,
|
|
4574
|
+
locale: NS,
|
|
4575
|
+
inject: () => ({ ctx }),
|
|
4576
|
+
},
|
|
4577
|
+
SafeSettingsCard,
|
|
4578
|
+
)
|
|
4579
|
+
if (!cardSlot) cardSlot = 'plugins.row.config'
|
|
4580
|
+
})
|
|
4581
|
+
|
|
4582
|
+
// 2. plugins.item (Official group on Plugins page)
|
|
4583
|
+
trySlot('plugins.item', () => {
|
|
4584
|
+
ctx.slots.register(
|
|
4585
|
+
{
|
|
4524
4586
|
name: 'plugins.item',
|
|
4525
4587
|
id: ROW_ID,
|
|
4526
4588
|
order: 60,
|
|
4527
4589
|
label: () => 'Kanban',
|
|
4528
4590
|
locale: NS,
|
|
4529
4591
|
inject: () => ({ ctx }),
|
|
4530
|
-
},
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
}
|
|
4592
|
+
},
|
|
4593
|
+
SafeSettingsCard,
|
|
4594
|
+
)
|
|
4595
|
+
})
|
|
4535
4596
|
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4597
|
+
// 3. settings.plugin.item (legacy fallback for older cores)
|
|
4598
|
+
trySlot('settings.plugin.item', () => {
|
|
4599
|
+
ctx.slots.register(
|
|
4600
|
+
{
|
|
4601
|
+
name: 'settings.plugin.item',
|
|
4602
|
+
key: NS,
|
|
4603
|
+
locale: NS,
|
|
4604
|
+
inject: () => ({ ctx }),
|
|
4605
|
+
},
|
|
4606
|
+
SafeSettingsCard,
|
|
4607
|
+
)
|
|
4608
|
+
if (!cardSlot) cardSlot = 'settings.plugin.item'
|
|
4609
|
+
})
|
|
4540
4610
|
|
|
4541
4611
|
const toggle = createToggle()
|
|
4542
4612
|
ctx.effect(() => {
|
package/lib/commands.js
CHANGED
|
@@ -51,6 +51,7 @@ const TABLE = {
|
|
|
51
51
|
* `undefined` — такой колонки на этой доске нет.
|
|
52
52
|
*/
|
|
53
53
|
export function commandFor(column, kind = 'project') {
|
|
54
|
+
if (!columnsOfKind(kind).includes(column)) return undefined
|
|
54
55
|
const board = TABLE[kind]
|
|
55
56
|
if (board === undefined) return undefined
|
|
56
57
|
const found = board[column]
|
package/lib/gitea.js
CHANGED
|
@@ -33,7 +33,9 @@ export function safeSegment(value) {
|
|
|
33
33
|
|
|
34
34
|
/** Имя учётной записи — имя переменной окружения, а не сам секрет. */
|
|
35
35
|
export function isCredentialName(value) {
|
|
36
|
-
|
|
36
|
+
const str = String(value || '')
|
|
37
|
+
if (looksLikeSecret(str)) return false
|
|
38
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str)
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Похоже на сам токен, а не на его имя. */
|
package/lib/index.js
CHANGED
|
@@ -77,6 +77,9 @@ import { planOutbound, planAssign, createOutbox } from './outbound.js'
|
|
|
77
77
|
import { computeNextRun } from './cron.js'
|
|
78
78
|
import { PowerInhibitor } from './power.js'
|
|
79
79
|
import { formatProgressDump, parseProgressDump } from './progress-dump.js'
|
|
80
|
+
import { randomUUID } from 'node:crypto'
|
|
81
|
+
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
82
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
80
83
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
81
84
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
82
85
|
import z from '@deepseek-ai/schemastery'
|
|
@@ -92,7 +95,7 @@ import {
|
|
|
92
95
|
rootOf,
|
|
93
96
|
} from './config.js'
|
|
94
97
|
import { eventHub } from './events.js'
|
|
95
|
-
import { TASK_TEMPLATES } from './templates.js'
|
|
98
|
+
import { TASK_TEMPLATES, getTemplateById } from './templates.js'
|
|
96
99
|
|
|
97
100
|
export { COLUMN_ORDER, wipLimitField, withDefaults } from './config.js'
|
|
98
101
|
|
|
@@ -838,10 +841,25 @@ export function apply(ctx, config) {
|
|
|
838
841
|
},
|
|
839
842
|
}), 'dsh-kanban: sse events stream')
|
|
840
843
|
|
|
844
|
+
// Очистка SSE-клиентов и heartbeat таймера при выгрузке плагина (#282)
|
|
845
|
+
if (typeof ctx.on === 'function') {
|
|
846
|
+
ctx.on('dispose', () => {
|
|
847
|
+
eventHub.closeAll()
|
|
848
|
+
})
|
|
849
|
+
}
|
|
850
|
+
|
|
841
851
|
ctx.effect(() => ctx.webServer.register({
|
|
842
852
|
kind: 'exact',
|
|
843
853
|
path: '/dsh-kanban/templates',
|
|
844
854
|
handler: (req, res) => {
|
|
855
|
+
const u = new URL(req.url, 'http://localhost')
|
|
856
|
+
const tid = u.searchParams.get('id')
|
|
857
|
+
if (tid) {
|
|
858
|
+
const tmpl = getTemplateById(tid)
|
|
859
|
+
if (!tmpl) { writeJson(res, 404, { error: 'template-not-found' }); return }
|
|
860
|
+
reply(res, { template: tmpl })
|
|
861
|
+
return
|
|
862
|
+
}
|
|
845
863
|
reply(res, { templates: TASK_TEMPLATES })
|
|
846
864
|
},
|
|
847
865
|
}), 'dsh-kanban: task templates')
|
|
@@ -1098,16 +1116,15 @@ export function apply(ctx, config) {
|
|
|
1098
1116
|
config: getConfig(),
|
|
1099
1117
|
provider: picked.provider,
|
|
1100
1118
|
model: picked.model,
|
|
1101
|
-
mintSessionId,
|
|
1102
|
-
createMessage,
|
|
1103
|
-
cwdOf,
|
|
1119
|
+
mintSessionId: () => SessionId(`kanban-${id}-${randomUUID()}`),
|
|
1120
|
+
createMessage: createUserMessage,
|
|
1121
|
+
cwdOf: undefined,
|
|
1104
1122
|
logger: ctx.logger,
|
|
1105
|
-
text: input?.text,
|
|
1123
|
+
text: typeof input?.text === 'string' ? input.text : undefined,
|
|
1106
1124
|
agentPreset: input?.agentPreset || task.agentPreset,
|
|
1107
1125
|
permission: input?.permission || task.permission,
|
|
1108
1126
|
permissions: ctx.get('permissionPresets'),
|
|
1109
|
-
claimAs: req.headers['x-dsh-user'],
|
|
1110
|
-
gitRunner,
|
|
1127
|
+
claimAs: req.headers['x-dsh-user'] || (await whoAmI()),
|
|
1111
1128
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
1112
1129
|
resume: true,
|
|
1113
1130
|
})
|
|
@@ -1281,7 +1298,7 @@ export function apply(ctx, config) {
|
|
|
1281
1298
|
handler: (req, res) => {
|
|
1282
1299
|
const m = /^\/dsh-kanban\/session\/([^/]+)\/task\/?$/.exec(urlOf(req).pathname)
|
|
1283
1300
|
if (m === null) { writeJson(res, 404, { error: 'not-found' }); return }
|
|
1284
|
-
reply(res, taskBySession({ store, sessionId: decodeURIComponent(m[1]) }))
|
|
1301
|
+
reply(res, taskBySession({ store, sessionId: decodeURIComponent(m[1]), agents: ctx.agents }))
|
|
1285
1302
|
},
|
|
1286
1303
|
}), 'dsh-kanban: task route by session')
|
|
1287
1304
|
|
package/lib/launcher.js
CHANGED
|
@@ -416,7 +416,7 @@ export async function runTask({
|
|
|
416
416
|
})
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
return { sessionId: taken.sessionId, mode: taken.mode, task: current }
|
|
419
|
+
return { sessionId: taken.sessionId, mode: revivalKind(taken.mode), task: current }
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
/** Текст сообщения: человеческий, если он есть, иначе встроенная заготовка. */
|
package/lib/markdown.js
CHANGED
|
@@ -119,7 +119,7 @@ export function parseSpans(text) {
|
|
|
119
119
|
const href = piece.slice(cut + 2, -1)
|
|
120
120
|
// Небезопасная схема — не ссылка, а текст. Молча выбрасывать нельзя:
|
|
121
121
|
// человек должен видеть, что там было написано.
|
|
122
|
-
if (
|
|
122
|
+
if (isSafeHref(href)) out.push({ kind: 'link', text: label, href })
|
|
123
123
|
else out.push({ kind: 'text', text: piece })
|
|
124
124
|
} else if (piece.startsWith('**') || piece.startsWith('__')) {
|
|
125
125
|
out.push({ kind: 'strong', text: piece.slice(2, -2) })
|
package/lib/routes.js
CHANGED
|
@@ -402,12 +402,18 @@ export function taskLog({ store, id }) {
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
/** Задача, привязанная к сессии; отсутствие задачи — штатный ответ. */
|
|
405
|
-
export function taskBySession({ store, sessionId }) {
|
|
405
|
+
export function taskBySession({ store, sessionId, agents }) {
|
|
406
406
|
const task = store.findTaskBySession(sessionId)
|
|
407
407
|
if (task === undefined) return { task: null }
|
|
408
|
+
let live
|
|
409
|
+
try { live = agents?.get?.(sessionId) } catch { live = undefined }
|
|
408
410
|
// Вид доски едет вместе с задачей: чип предлагает колонки, а предлагать
|
|
409
411
|
// `deploy` задаче с простой доски значит звать в колонку, которой нет.
|
|
410
|
-
return {
|
|
412
|
+
return {
|
|
413
|
+
task,
|
|
414
|
+
columns: columnsOf(store.boardKind(task.board)),
|
|
415
|
+
canRevive: canRevive(task, live),
|
|
416
|
+
}
|
|
411
417
|
}
|
|
412
418
|
|
|
413
419
|
/**
|
|
@@ -443,34 +449,42 @@ export function importBoard({ store, input }) {
|
|
|
443
449
|
let imported = 0
|
|
444
450
|
let skipped = 0
|
|
445
451
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
452
|
+
const doImport = () => {
|
|
453
|
+
for (const board of Array.isArray(input.boards) ? input.boards : []) {
|
|
454
|
+
if (typeof board?.id !== 'string' || board.id === '') continue
|
|
455
|
+
store.createBoard({ id: board.id, title: board.title, kind: board.kind })
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const task of input.tasks) {
|
|
459
|
+
if (typeof task?.id !== 'string' || task.id === '') continue
|
|
460
|
+
if (store.getTask(task.id) !== undefined) { skipped += 1; continue }
|
|
461
|
+
store.createTask({
|
|
462
|
+
id: task.id,
|
|
463
|
+
board: task.board ?? 'main',
|
|
464
|
+
column: task.column ?? 'backlog',
|
|
465
|
+
title: task.title ?? '',
|
|
466
|
+
body: task.body ?? '',
|
|
467
|
+
owner: task.owner, repo: task.repo,
|
|
468
|
+
issueNumber: task.issueNumber, issueUrl: task.issueUrl,
|
|
469
|
+
labels: Array.isArray(task.labels) ? task.labels : [],
|
|
470
|
+
branch: task.branch, worktree: task.worktree,
|
|
471
|
+
sessionId: task.sessionId, model: task.model, provider: task.provider,
|
|
472
|
+
waiting: task.waiting === true,
|
|
473
|
+
plan: task.plan ?? '',
|
|
474
|
+
labelColors: task.labelColors ?? {},
|
|
475
|
+
author: task.author ?? '',
|
|
476
|
+
priority: task.priority ?? '',
|
|
477
|
+
dueAt: typeof task.dueAt === 'number' ? task.dueAt : 0,
|
|
478
|
+
createdAt: task.createdAt,
|
|
479
|
+
})
|
|
480
|
+
imported += 1
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (typeof store.withTransaction === 'function') {
|
|
485
|
+
store.withTransaction(doImport)
|
|
486
|
+
} else {
|
|
487
|
+
doImport()
|
|
474
488
|
}
|
|
475
489
|
|
|
476
490
|
return { imported, skipped, backup: backupMeta?.backup }
|
package/lib/store.js
CHANGED
|
@@ -220,11 +220,20 @@ export function openStore({ dir }) {
|
|
|
220
220
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_cron_next ON tasks(archivedAt, nextRunAt)')
|
|
221
221
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_queued ON tasks(archivedAt, queuedAt)')
|
|
222
222
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_parent_id ON tasks(parentId)')
|
|
223
|
+
db.exec('CREATE INDEX IF NOT EXISTS tasks_updated ON tasks(updatedAt)')
|
|
224
|
+
db.exec('CREATE INDEX IF NOT EXISTS tasks_archivable ON tasks(archivedAt, col, columnAt)')
|
|
223
225
|
db.exec('PRAGMA synchronous = NORMAL')
|
|
224
226
|
try { db.exec('PRAGMA optimize') } catch { /* pragma optimize best-effort */ }
|
|
225
227
|
|
|
226
228
|
const q = {
|
|
227
229
|
boards: db.prepare('SELECT * FROM boards ORDER BY position'),
|
|
230
|
+
insertTask: db.prepare(`INSERT INTO tasks
|
|
231
|
+
(id, board, col, position, title, body, owner, repo, issueNumber, issueUrl, externalRef,
|
|
232
|
+
labels, branch, worktree, workspaceId, sessionId, model, provider, waiting, syncedAt,
|
|
233
|
+
labelColors, author, assignee, milestone, priority, dueAt, columnAt, createdAt, updatedAt,
|
|
234
|
+
cron, nextRunAt, lastRunAt, progressDump, permission, permissionConfirmedAt, runs,
|
|
235
|
+
checklist, report, comments, rejectReason, rejectedAt, reasoningEffort, parentId, isEpic)
|
|
236
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
228
237
|
boardById: db.prepare('SELECT * FROM boards WHERE id = ?'),
|
|
229
238
|
insertBoard: db.prepare('INSERT INTO boards (id, title, kind, position, createdAt) VALUES (?, ?, ?, ?, ?)'),
|
|
230
239
|
lastBoard: db.prepare('SELECT position FROM boards ORDER BY position DESC LIMIT 1'),
|
|
@@ -285,6 +294,18 @@ export function openStore({ dir }) {
|
|
|
285
294
|
}
|
|
286
295
|
|
|
287
296
|
return {
|
|
297
|
+
withTransaction(fn) {
|
|
298
|
+
db.exec('BEGIN IMMEDIATE')
|
|
299
|
+
try {
|
|
300
|
+
const res = fn()
|
|
301
|
+
db.exec('COMMIT')
|
|
302
|
+
return res
|
|
303
|
+
} catch (err) {
|
|
304
|
+
db.exec('ROLLBACK')
|
|
305
|
+
throw err
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
|
|
288
309
|
backupDatabase() {
|
|
289
310
|
const backupsDir = join(dir, 'backups')
|
|
290
311
|
mkdirSync(backupsDir, { recursive: true })
|
|
@@ -378,13 +399,7 @@ export function openStore({ dir }) {
|
|
|
378
399
|
// генерирует новый.
|
|
379
400
|
const id = typeof input.id === 'string' && input.id !== '' ? input.id : randomUUID()
|
|
380
401
|
const position = positionFor(board, column, input.beforeId, input.afterId)
|
|
381
|
-
|
|
382
|
-
(id, board, col, position, title, body, owner, repo, issueNumber, issueUrl, externalRef,
|
|
383
|
-
labels, branch, worktree, workspaceId, sessionId, model, provider, waiting, syncedAt,
|
|
384
|
-
labelColors, author, assignee, milestone, priority, dueAt, columnAt, createdAt, updatedAt,
|
|
385
|
-
cron, nextRunAt, lastRunAt, progressDump, permission, permissionConfirmedAt, runs,
|
|
386
|
-
checklist, report, comments, rejectReason, rejectedAt, reasoningEffort, parentId, isEpic)
|
|
387
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
402
|
+
q.insertTask.run(
|
|
388
403
|
id, board, column, position,
|
|
389
404
|
input.title ?? '', input.body ?? '',
|
|
390
405
|
input.owner ?? null, input.repo ?? null,
|
package/lib/time.js
CHANGED
|
@@ -40,6 +40,7 @@ export function isStale({ now, updatedAt, sessionId, afterMinutes }) {
|
|
|
40
40
|
const limit = Number(afterMinutes)
|
|
41
41
|
if (!Number.isFinite(limit) || limit <= 0) return false
|
|
42
42
|
if (typeof sessionId !== 'string' || sessionId === '') return false
|
|
43
|
-
|
|
43
|
+
const parts = relativeParts(now, updatedAt)
|
|
44
|
+
if (!parts) return false
|
|
44
45
|
return now - updatedAt >= limit * MINUTE
|
|
45
46
|
}
|
package/lib/worktree.js
CHANGED
|
@@ -168,7 +168,12 @@ export async function createTaskWorktree({
|
|
|
168
168
|
env,
|
|
169
169
|
cwdOf,
|
|
170
170
|
}) {
|
|
171
|
-
if (!repoDir)
|
|
171
|
+
if (!repoDir) {
|
|
172
|
+
if (task?.workspacePath) {
|
|
173
|
+
return prepareMultiRepoMirror({ task, workspacePath: task.workspacePath, gitRunner, config })
|
|
174
|
+
}
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
172
177
|
const isGit = await isGitRepo(repoDir, gitRunner)
|
|
173
178
|
if (!isGit) return undefined
|
|
174
179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "Visual Kanban board for DeepSeek Harness: Gitea-backed tasks, workflow columns, and a dedicated agent session per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|