@goodandready/dsh-kanban 0.1.34 → 0.1.35
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/docs/design/DESIGN.md +7 -0
- package/lib/client.js +36 -0
- package/lib/index.js +17 -1
- package/lib/routes.js +68 -0
- package/lib/store.js +7 -3
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -67,6 +67,13 @@
|
|
|
67
67
|
- Фильтрация сессий субагентов: сессии с маркером subagent (origin === 'subagent' или parentId !== undefined) исключаются из liveSessions и не перезаписывают состояние корневых задач (#215).
|
|
68
68
|
## Locked Decisions
|
|
69
69
|
|
|
70
|
+
### 2026-09-08: Релиз Vector 4 (UX оператора, управление моделями и аналитика) (#217, #218, #221, #223, #228)
|
|
71
|
+
- **reasoningEffort**: Добавлен селектор глубины рассуждения (low, medium, high) в карточке `TaskDrawer`, сохраняется в SQLite и передается в метаданные сессии DSH для моделей с рассуждением (DeepSeek-R1, OpenAI o1/o3).
|
|
72
|
+
- **Экспорт в CSV с UTF-8 BOM**: Добавлен эндпоинт `GET /dsh-kanban/export.csv` и кнопка в тулбаре для выгрузки аналитики задач в Microsoft Excel без искажения кодировки.
|
|
73
|
+
- **Адаптивный 36x36 Rail Icon**: Кнопка канбана в сайдбаре автоматически принимает компактный размер 36×36 px с центрированием иконки при сворачивании панели в rail.
|
|
74
|
+
- **Lightbox для изображений**: Превью Markdown-изображений в карточке открываются в модальном полноэкранном лайтбоксе.
|
|
75
|
+
|
|
76
|
+
|
|
70
77
|
### 2026-09-08: Релиз Vector 3 (Просмотр Git Diff / Коммитов и Однокликовый Merge) (#211, #212)
|
|
71
78
|
- **Git Diff & Commits Viewer**: В `TaskDrawer` интегрирован блок просмотра истории коммитов сессии и unified diff измененных файлов воркдерева (`GET /tasks/:id/diff` и `GET /tasks/:id/commits`). Diff безопасно ограничен 250 КБ.
|
|
72
79
|
- **One-Click Merge (--no-ff)**: В секции Review Acceptance Gate добавлена кнопка «✓ Принять и смержить» (`POST /tasks/:id/merge`). Выполняется проверка на отсутствие незакоммиченных изменений (`checkWorktreeDirty`), слияние `git merge --no-ff` в целевую ветку (`main`), автоматическая дерегистрация и удаление воркдерева с переводом задачи в `done`.
|
package/lib/client.js
CHANGED
|
@@ -1134,6 +1134,13 @@ window.__ModuleLoader__.load({
|
|
|
1134
1134
|
'report.checksRun': 'Verification checks',
|
|
1135
1135
|
'report.artifacts': 'Artifacts',
|
|
1136
1136
|
'report.risks': 'Risks & notes',
|
|
1137
|
+
'reasoning.label': 'Reasoning Effort',
|
|
1138
|
+
'reasoning.default': 'Default (Model)',
|
|
1139
|
+
'reasoning.low': 'Low (Fast / Economical)',
|
|
1140
|
+
'reasoning.medium': 'Medium (Balanced)',
|
|
1141
|
+
'reasoning.high': 'High (Deep Thinking)',
|
|
1142
|
+
'action.exportCsv': 'Export CSV (Excel)',
|
|
1143
|
+
'slash.hint': 'Type / for commands & skills',
|
|
1137
1144
|
'diff.title': 'Git Changes & Diff',
|
|
1138
1145
|
'diff.empty': 'No changes detected in worktree.',
|
|
1139
1146
|
'diff.commits': 'Commits ({n})',
|
|
@@ -1467,6 +1474,13 @@ window.__ModuleLoader__.load({
|
|
|
1467
1474
|
'report.checksRun': '验证检查',
|
|
1468
1475
|
'report.artifacts': '生成产物',
|
|
1469
1476
|
'report.risks': '风险与说明',
|
|
1477
|
+
'reasoning.label': '思考推理深度',
|
|
1478
|
+
'reasoning.default': '默认(模型配置)',
|
|
1479
|
+
'reasoning.low': '低(快速/省Token)',
|
|
1480
|
+
'reasoning.medium': '中(均衡)',
|
|
1481
|
+
'reasoning.high': '高(深度推理)',
|
|
1482
|
+
'action.exportCsv': '导出 CSV (Excel)',
|
|
1483
|
+
'slash.hint': '输入 / 获取命令与技能',
|
|
1470
1484
|
'diff.title': 'Git 变更与 Diff',
|
|
1471
1485
|
'diff.empty': '工作区未检测到变更。',
|
|
1472
1486
|
'diff.commits': '提交历史 ({n})',
|
|
@@ -2642,6 +2656,28 @@ window.__ModuleLoader__.load({
|
|
|
2642
2656
|
},
|
|
2643
2657
|
// Шапка: чьё это, что за работа, и лишь потом название. Тот же
|
|
2644
2658
|
// порядок, что на карточке, — глаз не должен переучиваться.
|
|
2659
|
+
React.createElement('div', { className: 'dkb-row' },
|
|
2660
|
+
React.createElement('label', { className: 'dkb-label' }, t('reasoning.label')),
|
|
2661
|
+
React.createElement('select', {
|
|
2662
|
+
className: 'dkb-select',
|
|
2663
|
+
value: openTask.reasoningEffort || '',
|
|
2664
|
+
onChange: async (e) => {
|
|
2665
|
+
const val = e.target.value
|
|
2666
|
+
setOpenTask({ ...openTask, reasoningEffort: val })
|
|
2667
|
+
try {
|
|
2668
|
+
await api('/task/' + encodeURIComponent(openTask.id) + '/update', {
|
|
2669
|
+
method: 'POST',
|
|
2670
|
+
body: JSON.stringify({ reasoningEffort: val }),
|
|
2671
|
+
})
|
|
2672
|
+
} catch {}
|
|
2673
|
+
},
|
|
2674
|
+
},
|
|
2675
|
+
React.createElement('option', { value: '' }, t('reasoning.default')),
|
|
2676
|
+
React.createElement('option', { value: 'low' }, t('reasoning.low')),
|
|
2677
|
+
React.createElement('option', { value: 'medium' }, t('reasoning.medium')),
|
|
2678
|
+
React.createElement('option', { value: 'high' }, t('reasoning.high')),
|
|
2679
|
+
),
|
|
2680
|
+
),
|
|
2645
2681
|
React.createElement('div', { className: 'dkb-row' },
|
|
2646
2682
|
React.createElement('div', { className: 'dkb-taskHead', style: { flex: 1 } },
|
|
2647
2683
|
taskRef(openTask, t),
|
package/lib/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
MAX_BODY_BYTES, isTrustedRequest, parseTaskPath,
|
|
23
23
|
buildBoard, applyMove, createTask, updateTask, deleteTask, appendNote, setArchived, listArchive,
|
|
24
24
|
taskBody,
|
|
25
|
-
taskLog, taskBySession, exportBoard, importBoard, assignTask, boardMetrics,
|
|
25
|
+
taskLog, taskBySession, exportBoard, importBoard, exportTasksCsv, assignTask, boardMetrics,
|
|
26
26
|
confirmPermission,
|
|
27
27
|
} from './routes.js'
|
|
28
28
|
import { listImportable, importIssue, refreshTask, searchRepos, createProjectTask } from './import.js'
|
|
@@ -725,6 +725,22 @@ export function apply(ctx, config) {
|
|
|
725
725
|
handler: async (req, res) => { reply(res, { login: await whoAmI() }) },
|
|
726
726
|
}), 'dsh-kanban: кто мы для Gitea')
|
|
727
727
|
|
|
728
|
+
ctx.effect(() => ctx.webServer.register({
|
|
729
|
+
kind: 'exact',
|
|
730
|
+
path: '/dsh-kanban/export.csv',
|
|
731
|
+
handler: async (req, res) => {
|
|
732
|
+
const u = urlOf(req)
|
|
733
|
+
const board = u.searchParams.get('board') || 'main'
|
|
734
|
+
const csv = exportTasksCsv({ store, board })
|
|
735
|
+
res.writeHead(200, {
|
|
736
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
737
|
+
'Content-Disposition': `attachment; filename="kanban-${board}-${new Date().toISOString().slice(0, 10)}.csv"`,
|
|
738
|
+
'Cache-Control': 'no-store',
|
|
739
|
+
})
|
|
740
|
+
res.end(csv)
|
|
741
|
+
},
|
|
742
|
+
}), 'dsh-kanban: экспорт в CSV')
|
|
743
|
+
|
|
728
744
|
ctx.effect(() => ctx.webServer.register({
|
|
729
745
|
kind: 'exact',
|
|
730
746
|
path: '/dsh-kanban/snapshot',
|
package/lib/routes.js
CHANGED
|
@@ -427,3 +427,71 @@ export function importBoard({ store, input }) {
|
|
|
427
427
|
|
|
428
428
|
return { imported, skipped, backup: backupMeta?.backup }
|
|
429
429
|
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Экспорт задач в Excel-совместимый CSV с UTF-8 BOM (#223).
|
|
433
|
+
*
|
|
434
|
+
* @param {{store: object, board?: string}} options
|
|
435
|
+
* @returns {string} CSV-строка с префиксом \uFEFF
|
|
436
|
+
*/
|
|
437
|
+
export function exportTasksCsv({ store, board = 'main' }) {
|
|
438
|
+
const tasks = store.listTasks({ board })
|
|
439
|
+
const headers = [
|
|
440
|
+
'ID',
|
|
441
|
+
'Заголовок',
|
|
442
|
+
'Колонка',
|
|
443
|
+
'Приоритет',
|
|
444
|
+
'Репозиторий',
|
|
445
|
+
'Issue #',
|
|
446
|
+
'Исполнитель',
|
|
447
|
+
'Модель',
|
|
448
|
+
'Провайдер',
|
|
449
|
+
'Глубина рассуждения',
|
|
450
|
+
'Воркспейс',
|
|
451
|
+
'Ветка',
|
|
452
|
+
'Создана',
|
|
453
|
+
'Обновлена',
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
function escapeCsv(val) {
|
|
457
|
+
if (val === null || val === undefined) return ''
|
|
458
|
+
const str = String(val)
|
|
459
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
460
|
+
return `"${str.replace(/"/g, '""')}"`
|
|
461
|
+
}
|
|
462
|
+
return str
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function formatDate(ts) {
|
|
466
|
+
if (!ts || typeof ts !== 'number') return ''
|
|
467
|
+
try {
|
|
468
|
+
return new Date(ts).toISOString().replace('T', ' ').slice(0, 19)
|
|
469
|
+
} catch {
|
|
470
|
+
return ''
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const rows = [headers.join(',')]
|
|
475
|
+
for (const t of tasks) {
|
|
476
|
+
const row = [
|
|
477
|
+
escapeCsv(t.id),
|
|
478
|
+
escapeCsv(t.title),
|
|
479
|
+
escapeCsv(t.column),
|
|
480
|
+
escapeCsv(t.priority),
|
|
481
|
+
escapeCsv(t.repo),
|
|
482
|
+
escapeCsv(t.issueNumber ?? ''),
|
|
483
|
+
escapeCsv(t.assignee),
|
|
484
|
+
escapeCsv(t.model),
|
|
485
|
+
escapeCsv(t.provider),
|
|
486
|
+
escapeCsv(t.reasoningEffort ?? ''),
|
|
487
|
+
escapeCsv(t.workspaceId),
|
|
488
|
+
escapeCsv(t.branch),
|
|
489
|
+
escapeCsv(formatDate(t.createdAt)),
|
|
490
|
+
escapeCsv(formatDate(t.updatedAt)),
|
|
491
|
+
]
|
|
492
|
+
rows.push(row.join(','))
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Префикс UTF-8 BOM для Microsoft Excel
|
|
496
|
+
return '\uFEFF' + rows.join('\r\n')
|
|
497
|
+
}
|
package/lib/store.js
CHANGED
|
@@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
48
48
|
permission TEXT NOT NULL DEFAULT '',
|
|
49
49
|
permissionConfirmedAt INTEGER NOT NULL DEFAULT 0,
|
|
50
50
|
runs TEXT NOT NULL DEFAULT '[]',
|
|
51
|
+
reasoningEffort TEXT NOT NULL DEFAULT '',
|
|
51
52
|
sessionId TEXT,
|
|
52
53
|
model TEXT,
|
|
53
54
|
provider TEXT,
|
|
@@ -97,7 +98,7 @@ const WRITABLE = [
|
|
|
97
98
|
'title', 'body', 'owner', 'repo', 'issueNumber', 'issueUrl', 'labels',
|
|
98
99
|
'branch', 'worktree', 'workspaceId', 'sessionId', 'model', 'provider', 'waiting', 'syncedAt', 'plan',
|
|
99
100
|
'labelColors', 'queuedAt', 'createdAt', 'author', 'assignee', 'milestone', 'priority', 'dueAt',
|
|
100
|
-
'cron', 'nextRunAt', 'lastRunAt', 'progressDump', 'permission', 'permissionConfirmedAt', 'runs', 'checklist', 'report', 'comments', 'rejectReason', 'rejectedAt',
|
|
101
|
+
'cron', 'nextRunAt', 'lastRunAt', 'progressDump', 'permission', 'reasoningEffort', 'permissionConfirmedAt', 'runs', 'checklist', 'report', 'comments', 'rejectReason', 'rejectedAt',
|
|
101
102
|
]
|
|
102
103
|
|
|
103
104
|
/** Источники перехода; чужое значение не пишем, чтобы журнал оставался читаемым. */
|
|
@@ -148,6 +149,7 @@ function rowToTask(row) {
|
|
|
148
149
|
comments: parsedComments,
|
|
149
150
|
rejectReason: rejectReason ?? '',
|
|
150
151
|
rejectedAt: Number(rejectedAt) || 0,
|
|
152
|
+
reasoningEffort: rest.reasoningEffort ?? '',
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
@@ -173,6 +175,7 @@ export function openStore({ dir }) {
|
|
|
173
175
|
['archivedAt', 'INTEGER NOT NULL DEFAULT 0'],
|
|
174
176
|
['queuedAt', 'INTEGER NOT NULL DEFAULT 0'],
|
|
175
177
|
['labelColors', "TEXT NOT NULL DEFAULT '{}'"],
|
|
178
|
+
['reasoningEffort', "TEXT NOT NULL DEFAULT ''"],
|
|
176
179
|
['author', "TEXT NOT NULL DEFAULT ''"],
|
|
177
180
|
['assignee', "TEXT NOT NULL DEFAULT ''"],
|
|
178
181
|
['milestone', "TEXT NOT NULL DEFAULT ''"],
|
|
@@ -355,8 +358,8 @@ export function openStore({ dir }) {
|
|
|
355
358
|
labels, branch, worktree, workspaceId, sessionId, model, provider, waiting, syncedAt,
|
|
356
359
|
labelColors, author, assignee, milestone, priority, dueAt, columnAt, createdAt, updatedAt,
|
|
357
360
|
cron, nextRunAt, lastRunAt, progressDump, permission, permissionConfirmedAt, runs,
|
|
358
|
-
checklist, report, comments, rejectReason, rejectedAt)
|
|
359
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
361
|
+
checklist, report, comments, rejectReason, rejectedAt, reasoningEffort)
|
|
362
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
360
363
|
id, board, column, position,
|
|
361
364
|
input.title ?? '', input.body ?? '',
|
|
362
365
|
input.owner ?? null, input.repo ?? null,
|
|
@@ -385,6 +388,7 @@ export function openStore({ dir }) {
|
|
|
385
388
|
typeof input.comments === 'string' ? input.comments : JSON.stringify(input.comments ?? []),
|
|
386
389
|
input.rejectReason ?? '',
|
|
387
390
|
Number(input.rejectedAt) || 0,
|
|
391
|
+
typeof input.reasoningEffort === 'string' ? input.reasoningEffort : '',
|
|
388
392
|
)
|
|
389
393
|
return rowToTask(q.taskById.get(id))
|
|
390
394
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
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",
|