@goodandready/dsh-kanban 0.1.18 → 0.1.20
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/README.md +21 -0
- package/lib/client.js +149 -1
- package/lib/filters.js +288 -243
- package/lib/index.js +34 -1
- package/lib/routes.js +331 -256
- package/lib/store.js +389 -366
- package/lib/transitions.js +162 -162
- package/package.json +1 -1
- /package/{README.ru.md → docs/README.ru.md} +0 -0
- /package/{README.zh.md → docs/README.zh.md} +0 -0
package/README.md
CHANGED
|
@@ -116,6 +116,27 @@ Unlike passive task boards, `dsh-kanban` actively drives the agents:
|
|
|
116
116
|
|
|
117
117
|
---
|
|
118
118
|
|
|
119
|
+
### 5. Card Detail: Labels, Priority and Deadlines
|
|
120
|
+
|
|
121
|
+
Cards carry more than a title. Labels and priority are editable right in the card
|
|
122
|
+
window — for a Gitea-backed task they travel back to the issue, for a board-only
|
|
123
|
+
task they live locally. Priority drives an "urgent first" ordering, so the column
|
|
124
|
+
answers "what now" without reading every card.
|
|
125
|
+
|
|
126
|
+
A due date can be set on any card. Overdue cards are highlighted, and a switch in
|
|
127
|
+
the board header narrows the board to them alone — the answer to "what is already
|
|
128
|
+
late" is one click, not a scan.
|
|
129
|
+
|
|
130
|
+
### 6. Board Snapshot: Export and Import
|
|
131
|
+
|
|
132
|
+
`GET /dsh-kanban/snapshot` returns the whole board as JSON; `POST` to the same
|
|
133
|
+
route restores it. Import is idempotent: a task whose id is already on the board
|
|
134
|
+
is skipped rather than duplicated, so re-running an import is safe.
|
|
135
|
+
|
|
136
|
+
The snapshot is one resource with two verbs on purpose. An earlier build gave the
|
|
137
|
+
upload the path `/dsh-kanban/import`, which the Gitea issue import already owned —
|
|
138
|
+
two handlers on one path mean one of them silently never answers.
|
|
139
|
+
|
|
119
140
|
## 📦 Installation
|
|
120
141
|
|
|
121
142
|
Install via DeepSeek Harness CLI:
|
package/lib/client.js
CHANGED
|
@@ -734,6 +734,8 @@ window.__ModuleLoader__.load({
|
|
|
734
734
|
'.dkb-count[data-col=deploy]{background:var(--dsw-alias-state-business-primary)}' +
|
|
735
735
|
'.dkb-count[data-col=done]{background:var(--dsw-alias-state-success-primary)}' +
|
|
736
736
|
'.dkb-countOver{background:var(--dsw-alias-state-error-primary);color:#fff}' +
|
|
737
|
+
'.dkb-overdue{border-color:var(--dsw-alias-state-error-primary)}' +
|
|
738
|
+
'.dkb-dueOver{color:var(--dsw-alias-state-error-primary);font-weight:600}' +
|
|
737
739
|
'.dkb-list{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}' +
|
|
738
740
|
'.dkb-empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:24px 8px;font-size:12px}' +
|
|
739
741
|
'.dkb-taskCard{text-align:left;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-primary);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;font-family:inherit;transition:box-shadow .12s,border-color .12s,transform .12s;display:flex}' +
|
|
@@ -894,6 +896,7 @@ window.__ModuleLoader__.load({
|
|
|
894
896
|
'board.expand': 'Expand the column',
|
|
895
897
|
'card.move': 'move',
|
|
896
898
|
'card.toChat': 'to the chat',
|
|
899
|
+
'card.due': 'due {date}',
|
|
897
900
|
'card.pick': 'Select for a group start',
|
|
898
901
|
'card.queue': 'to a queue...',
|
|
899
902
|
'card.unqueue': 'take out of the queue',
|
|
@@ -925,6 +928,11 @@ window.__ModuleLoader__.load({
|
|
|
925
928
|
'error.repo-not-created': 'The repository was not created; nothing else was done.',
|
|
926
929
|
'error.issue-not-created': 'The issue was not created. If the repository was new, it stayed — nothing was deleted.',
|
|
927
930
|
'board.archive': 'Archive',
|
|
931
|
+
'board.export': 'Download',
|
|
932
|
+
'board.import': 'Import',
|
|
933
|
+
'board.exportHint': 'Download the whole board as JSON',
|
|
934
|
+
'board.imported': 'Imported {n} tasks',
|
|
935
|
+
'error.badImport': 'That file is not a valid board backup.',
|
|
928
936
|
'panel.archive': 'To the archive',
|
|
929
937
|
'archive.hint': 'Cards that spent their term in Done. Nothing is deleted: a restored card returns to the column it left.',
|
|
930
938
|
'archive.empty': 'The archive is empty.',
|
|
@@ -1017,6 +1025,16 @@ window.__ModuleLoader__.load({
|
|
|
1017
1025
|
'panel.startHint': 'A dedicated agent session opens for this task. Branch and worktree are created by the agent after its preflight, not here.',
|
|
1018
1026
|
'panel.workDir': 'Working folder: {path}',
|
|
1019
1027
|
'panel.workDirHint': 'The project root is not set in settings, so this folder is used.',
|
|
1028
|
+
'panel.priority': 'Priority',
|
|
1029
|
+
'panel.priorityNone': 'No priority',
|
|
1030
|
+
'priority.high': 'High',
|
|
1031
|
+
'priority.medium': 'Medium',
|
|
1032
|
+
'priority.low': 'Low',
|
|
1033
|
+
'panel.labels': 'Labels',
|
|
1034
|
+
'panel.due': 'Due date',
|
|
1035
|
+
'panel.saveEdit': 'Save',
|
|
1036
|
+
'board.overdueCount': 'Overdue: {n}',
|
|
1037
|
+
'board.overdueHint': 'Show only overdue tasks',
|
|
1020
1038
|
'error.startFailed': 'Could not start the session.',
|
|
1021
1039
|
'error.sessionNotOpened': 'The session started ({id}) but this build cannot switch to it. Find it in the session list.',
|
|
1022
1040
|
'error.modelNotSelected': 'No model selected — pick one in Settings, or type it here.',
|
|
@@ -1123,6 +1141,7 @@ window.__ModuleLoader__.load({
|
|
|
1123
1141
|
'board.expand': '展开该列',
|
|
1124
1142
|
'card.move': '移动',
|
|
1125
1143
|
'card.toChat': '进入对话',
|
|
1144
|
+
'card.due': '截止 {date}',
|
|
1126
1145
|
'card.pick': '选中以便批量启动',
|
|
1127
1146
|
'card.queue': '加入队列…',
|
|
1128
1147
|
'card.unqueue': '移出队列',
|
|
@@ -1154,6 +1173,11 @@ window.__ModuleLoader__.load({
|
|
|
1154
1173
|
'error.repo-not-created': '仓库没有创建,其他动作也未执行。',
|
|
1155
1174
|
'error.issue-not-created': 'issue 没有创建。如果仓库是新建的,它仍然保留——没有删除任何东西。',
|
|
1156
1175
|
'board.archive': '归档',
|
|
1176
|
+
'board.export': '下载',
|
|
1177
|
+
'board.import': '导入',
|
|
1178
|
+
'board.exportHint': '将整个看板下载为 JSON',
|
|
1179
|
+
'board.imported': '已导入 {n} 个任务',
|
|
1180
|
+
'error.badImport': '该文件不是有效的看板备份。',
|
|
1157
1181
|
'panel.archive': '移入归档',
|
|
1158
1182
|
'archive.hint': '在「已完成」里待够时间的卡片。什么都没有删除:恢复的卡片会回到它离开的那一列。',
|
|
1159
1183
|
'archive.empty': '归档是空的。',
|
|
@@ -1246,6 +1270,16 @@ window.__ModuleLoader__.load({
|
|
|
1246
1270
|
'panel.startHint': '会为该任务打开一个专属的 Agent 会话。分支和 worktree 由 Agent 在自检之后创建,不在这里。',
|
|
1247
1271
|
'panel.workDir': '工作目录:{path}',
|
|
1248
1272
|
'panel.workDirHint': '设置中未指定项目根目录,因此使用此目录。',
|
|
1273
|
+
'panel.priority': '优先级',
|
|
1274
|
+
'panel.priorityNone': '无优先级',
|
|
1275
|
+
'priority.high': '高',
|
|
1276
|
+
'priority.medium': '中',
|
|
1277
|
+
'priority.low': '低',
|
|
1278
|
+
'panel.labels': '标签',
|
|
1279
|
+
'panel.due': '截止日期',
|
|
1280
|
+
'panel.saveEdit': '保存',
|
|
1281
|
+
'board.overdueCount': '已逾期:{n}',
|
|
1282
|
+
'board.overdueHint': '仅显示已逾期的任务',
|
|
1249
1283
|
'error.startFailed': '会话没能启动。',
|
|
1250
1284
|
'error.sessionNotOpened': '会话已启动({id}),但此版本无法切换过去。请到会话列表中查找。',
|
|
1251
1285
|
'error.modelNotSelected': '没有选择模型——请在设置中选择,或在此处填写。',
|
|
@@ -1481,8 +1515,10 @@ window.__ModuleLoader__.load({
|
|
|
1481
1515
|
const touched = task.sessionId ? agoText(now, task.updatedAt, t) : ''
|
|
1482
1516
|
const menu = props.menuOpen
|
|
1483
1517
|
const urgent = priorityColor(task)
|
|
1518
|
+
const overdue = task.overdue === true
|
|
1484
1519
|
return React.createElement('div', {
|
|
1485
1520
|
className: 'dkb-taskCard' + (urgent ? ' dkb-urgent' : '')
|
|
1521
|
+
+ (overdue ? ' dkb-overdue' : '')
|
|
1486
1522
|
+ (props.sameSession ? ' dkb-sameSession' : ''),
|
|
1487
1523
|
onMouseEnter: () => { if (props.onHover) props.onHover(task.sessionId || '') },
|
|
1488
1524
|
onMouseLeave: () => { if (props.onHover) props.onHover('') },
|
|
@@ -1581,6 +1617,12 @@ window.__ModuleLoader__.load({
|
|
|
1581
1617
|
born
|
|
1582
1618
|
? React.createElement('span', { title: exactAt(task.createdAt) }, born)
|
|
1583
1619
|
: null,
|
|
1620
|
+
task.dueAt > 0
|
|
1621
|
+
? React.createElement('span', {
|
|
1622
|
+
className: overdue ? 'dkb-dueOver' : '',
|
|
1623
|
+
title: exactAt(task.dueAt),
|
|
1624
|
+
}, t('card.due', { date: shortDate(task.dueAt) }))
|
|
1625
|
+
: null,
|
|
1584
1626
|
pack
|
|
1585
1627
|
? React.createElement('span', {
|
|
1586
1628
|
className: 'dkb-pack', title: t('card.packHint'),
|
|
@@ -1644,6 +1686,12 @@ window.__ModuleLoader__.load({
|
|
|
1644
1686
|
const [openTask, setOpenTask] = React.useState(null)
|
|
1645
1687
|
const [log, setLog] = React.useState([])
|
|
1646
1688
|
const [dialog, setDialog] = React.useState(null)
|
|
1689
|
+
// Черновик правки приоритета и меток в окне задачи. Живёт отдельно от
|
|
1690
|
+
// карточки: пока человек не нажал «Сохранить», доска не должна видеть
|
|
1691
|
+
// полувведённое.
|
|
1692
|
+
const [editPriority, setEditPriority] = React.useState('')
|
|
1693
|
+
const [editLabels, setEditLabels] = React.useState('')
|
|
1694
|
+
const [editDue, setEditDue] = React.useState('')
|
|
1647
1695
|
const [overColumn, setOverColumn] = React.useState('')
|
|
1648
1696
|
const [model, setModel] = React.useState('')
|
|
1649
1697
|
const [provider, setProvider] = React.useState('')
|
|
@@ -1673,6 +1721,7 @@ window.__ModuleLoader__.load({
|
|
|
1673
1721
|
// «Только ждущие» держим отдельно от отборов: это не значение измерения,
|
|
1674
1722
|
// а взгляд «покажи, где стоит работа», и складывается он с любым отбором.
|
|
1675
1723
|
const [onlyWaiting, setOnlyWaiting] = React.useState(false)
|
|
1724
|
+
const [onlyOverdue, setOnlyOverdue] = React.useState(false)
|
|
1676
1725
|
const [hoverSession, setHoverSession] = React.useState('')
|
|
1677
1726
|
const [order, setOrder] = React.useState('manual')
|
|
1678
1727
|
// Чем запускать: профиль агента и уровень доступа. Обе службы у ядра
|
|
@@ -1840,6 +1889,11 @@ window.__ModuleLoader__.load({
|
|
|
1840
1889
|
setOpenTask(task)
|
|
1841
1890
|
setDraftText('')
|
|
1842
1891
|
setBody(null)
|
|
1892
|
+
setEditPriority(typeof task?.priority === 'string' ? task.priority : '')
|
|
1893
|
+
setEditLabels(Array.isArray(task?.labels) ? task.labels.join(', ') : '')
|
|
1894
|
+
setEditDue(typeof task?.dueAt === 'number' && task.dueAt > 0
|
|
1895
|
+
? new Date(task.dueAt).toISOString().slice(0, 10)
|
|
1896
|
+
: '')
|
|
1843
1897
|
api('/task/' + encodeURIComponent(task.id) + '/body')
|
|
1844
1898
|
.then((out) => setBody((out && out.blocks) || []))
|
|
1845
1899
|
.catch(() => setBody([]))
|
|
@@ -1928,6 +1982,18 @@ window.__ModuleLoader__.load({
|
|
|
1928
1982
|
onClick: () => setOnlyWaiting((cur) => !cur),
|
|
1929
1983
|
}, t('board.waitCount', { n: waiting }))
|
|
1930
1984
|
})(),
|
|
1985
|
+
(() => {
|
|
1986
|
+
// Просроченные — те, чей дедлайн уже прошёл. Счётчик появляется
|
|
1987
|
+
// только когда такие есть: ноль в шапке — мебель.
|
|
1988
|
+
const overdue = state.tasks.filter((x) => x.overdue === true).length
|
|
1989
|
+
if (overdue === 0) return null
|
|
1990
|
+
return React.createElement('button', {
|
|
1991
|
+
type: 'button',
|
|
1992
|
+
className: 'dkb-waitCount' + (onlyOverdue ? ' dkb-save' : ''),
|
|
1993
|
+
title: t('board.overdueHint'),
|
|
1994
|
+
onClick: () => setOnlyOverdue((cur) => !cur),
|
|
1995
|
+
}, t('board.overdueCount', { n: overdue }))
|
|
1996
|
+
})(),
|
|
1931
1997
|
React.createElement('select', {
|
|
1932
1998
|
className: 'dkb-input', style: { width: 'auto' }, value: order,
|
|
1933
1999
|
onChange: (e) => setOrder(e.target.value),
|
|
@@ -1946,6 +2012,39 @@ window.__ModuleLoader__.load({
|
|
|
1946
2012
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
1947
2013
|
},
|
|
1948
2014
|
}, t('board.archive')),
|
|
2015
|
+
// Экспорт скачивает полный снимок доски; импорт восстанавливает его.
|
|
2016
|
+
React.createElement('button', {
|
|
2017
|
+
type: 'button', className: 'dkb-discard',
|
|
2018
|
+
title: t('board.exportHint'),
|
|
2019
|
+
onClick: async () => {
|
|
2020
|
+
try {
|
|
2021
|
+
const out = await api('/snapshot')
|
|
2022
|
+
const blob = new Blob([JSON.stringify(out, null, 2)], { type: 'application/json' })
|
|
2023
|
+
const url = URL.createObjectURL(blob)
|
|
2024
|
+
const a = document.createElement('a')
|
|
2025
|
+
a.href = url
|
|
2026
|
+
a.download = 'kanban-' + new Date().toISOString().slice(0, 10) + '.json'
|
|
2027
|
+
a.click()
|
|
2028
|
+
URL.revokeObjectURL(url)
|
|
2029
|
+
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2030
|
+
},
|
|
2031
|
+
}, t('board.export')),
|
|
2032
|
+
React.createElement('label', { className: 'dkb-discard dkb-import' },
|
|
2033
|
+
t('board.import'),
|
|
2034
|
+
React.createElement('input', {
|
|
2035
|
+
type: 'file', accept: 'application/json,.json', style: { display: 'none' },
|
|
2036
|
+
onChange: async (e) => {
|
|
2037
|
+
const file = e.target.files && e.target.files[0]
|
|
2038
|
+
e.target.value = ''
|
|
2039
|
+
if (!file) return
|
|
2040
|
+
try {
|
|
2041
|
+
const input = JSON.parse(await file.text())
|
|
2042
|
+
const out = await api('/snapshot', { method: 'POST', body: JSON.stringify(input) })
|
|
2043
|
+
setError(out.error ? t('error.' + out.error) : t('board.imported', { n: out.imported }))
|
|
2044
|
+
await load(board)
|
|
2045
|
+
} catch { setError(t('error.badImport')) }
|
|
2046
|
+
},
|
|
2047
|
+
})),
|
|
1949
2048
|
// «Показано 12 из 81» появляется, только когда что-то отобрано или
|
|
1950
2049
|
// введён поиск: на нетронутой доске эта строка была бы шумом.
|
|
1951
2050
|
anySelected(filters) || query.trim() !== ''
|
|
@@ -2087,7 +2186,8 @@ window.__ModuleLoader__.load({
|
|
|
2087
2186
|
const all = tasksOf(state.tasks, col.id).filter((task) => matchesFilters(task, filters))
|
|
2088
2187
|
const items = sortTasks(
|
|
2089
2188
|
all.filter((task) => matchesQuery(task, query))
|
|
2090
|
-
.filter((task) => !onlyWaiting || task.state === 'waiting')
|
|
2189
|
+
.filter((task) => !onlyWaiting || task.state === 'waiting')
|
|
2190
|
+
.filter((task) => !onlyOverdue || task.overdue === true),
|
|
2091
2191
|
order,
|
|
2092
2192
|
)
|
|
2093
2193
|
const limit = col.limit
|
|
@@ -2272,6 +2372,54 @@ window.__ModuleLoader__.load({
|
|
|
2272
2372
|
: body.length === 0
|
|
2273
2373
|
? null
|
|
2274
2374
|
: React.createElement('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
|
|
2375
|
+
// Приоритет и метки — локальное свойство доски: правятся здесь, а не
|
|
2376
|
+
// в Gitea. У задач из issue метки приезжают сверкой, но приоритет
|
|
2377
|
+
// всегда свой.
|
|
2378
|
+
React.createElement('div', { className: 'dkb-edit' },
|
|
2379
|
+
React.createElement('div', { className: 'dkb-field' },
|
|
2380
|
+
React.createElement('span', { className: 'dkb-label' }, t('panel.priority')),
|
|
2381
|
+
React.createElement('select', {
|
|
2382
|
+
className: 'dkb-input', value: editPriority,
|
|
2383
|
+
onChange: (e) => setEditPriority(e.target.value),
|
|
2384
|
+
},
|
|
2385
|
+
React.createElement('option', { value: '' }, t('panel.priorityNone')),
|
|
2386
|
+
['high', 'medium', 'low'].map((p) => React.createElement('option', { value: p, key: p }, t('priority.' + p)))),
|
|
2387
|
+
),
|
|
2388
|
+
React.createElement('div', { className: 'dkb-field' },
|
|
2389
|
+
React.createElement('span', { className: 'dkb-label' }, t('panel.labels')),
|
|
2390
|
+
React.createElement('input', {
|
|
2391
|
+
className: 'dkb-input', value: editLabels,
|
|
2392
|
+
placeholder: t('dialog.labelsHint'),
|
|
2393
|
+
onChange: (e) => setEditLabels(e.target.value),
|
|
2394
|
+
}),
|
|
2395
|
+
),
|
|
2396
|
+
React.createElement('div', { className: 'dkb-field' },
|
|
2397
|
+
React.createElement('span', { className: 'dkb-label' }, t('panel.due')),
|
|
2398
|
+
React.createElement('input', {
|
|
2399
|
+
type: 'date', className: 'dkb-input', value: editDue,
|
|
2400
|
+
onChange: (e) => setEditDue(e.target.value),
|
|
2401
|
+
}),
|
|
2402
|
+
),
|
|
2403
|
+
React.createElement('button', {
|
|
2404
|
+
type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
|
|
2405
|
+
onClick: async () => {
|
|
2406
|
+
try {
|
|
2407
|
+
const labels = editLabels.split(',').map((s) => s.trim()).filter(Boolean)
|
|
2408
|
+
// Пустая дата снимает дедлайн (0); заполненная — полночь
|
|
2409
|
+
// этого дня по местному времени.
|
|
2410
|
+
const dueAt = editDue === ''
|
|
2411
|
+
? 0
|
|
2412
|
+
: new Date(editDue + 'T00:00:00').getTime()
|
|
2413
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
|
|
2414
|
+
method: 'POST',
|
|
2415
|
+
body: JSON.stringify({ priority: editPriority, labels, dueAt }),
|
|
2416
|
+
})
|
|
2417
|
+
setOpenTask(out.task)
|
|
2418
|
+
await load(board)
|
|
2419
|
+
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2420
|
+
},
|
|
2421
|
+
}, t('panel.saveEdit')),
|
|
2422
|
+
),
|
|
2275
2423
|
openTask.sessionId
|
|
2276
2424
|
? React.createElement('button', {
|
|
2277
2425
|
type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
|